AngularJS Bootstrap 时间选择器 uib-timepicker 完全指南:配置参数、交互模式与源码原理

原创2026-09-21 15:42:421,672 阅读
文章标签:前端UI组件

AngularJS Bootstrap 时间选择器 uib-timepicker 完全指南:配置参数、交互模式与源码原理

本文以 src/timepicker/docs/readme.md 为主线,系统讲解 bootstrap 项目(AngularJS 版 Bootstrap 原生指令集)中 uib-timepicker 指令的全部可配置项、12H/24H 切换、步进交互、最小/最大时间约束与输入校验,并深入到 src/timepicker/timepicker.js 的控制器实现与 src/timepicker/test/timepicker.spec.js 的测试断言,帮助你在表单中快速集成一个轻量、可配置且行为可控的时间选择控件。

一、uib-timepicker 是什么

uib-timepicker 是一个轻量且高度可配置的时间选择指令,由属性型指令(restrict: 'A')承载,挂在任意块级元素上即可工作。它不依赖 jQuery 或 Bootstrap 的 JS 插件,只依赖 AngularJS 自身的双向绑定与表单校验机制。

从模块结构看,src/timepicker/index.js 引入样式并导出 src/timepicker/index-nocss.js,后者注册了 ui.bootstrap.module.timepicker 模块,内部同时依赖指令模块 ui.bootstrap.timepicker 与默认模板模块 uib/template/timepicker/timepicker.html。指令与模板分离的设计意味着你可以随时通过 template-url 替换外观,而不必改动指令逻辑。

二、快速上手:一个完整的示例

参考官方演示 src/timepicker/docs/demo.html 与 src/timepicker/docs/demo.js,最小用法只需绑定一个 Date 对象:

<div ng-controller="TimepickerDemoCtrl">
  <div uib-timepicker ng-model="mytime" ng-change="changed()"
       hour-step="hstep" minute-step="mstep" show-meridian="ismeridian"></div>
  <pre class="alert alert-info">Time is: {{mytime | date:'shortTime' }}</pre>
</div>

对应的控制器:

angular.module('ui.bootstrap.demo').controller('TimepickerDemoCtrl', function ($scope, $log) {
  $scope.mytime = new Date();

  $scope.hstep = 1;
  $scope.mstep = 15;

  $scope.options = {
    hstep: [1, 2, 3],
    mstep: [1, 5, 10, 15, 25, 30]
  };

  $scope.ismeridian = true;
  $scope.toggleMode = function() {
    $scope.ismeridian = ! $scope.ismeridian;
  };

  $scope.update = function() {
    var d = new Date();
    d.setHours( 14 );
    d.setMinutes( 0 );
    $scope.mytime = d;
  };

  $scope.changed = function () {
    $log.log('Time changed to: ' + $scope.mytime);
  };

  $scope.clear = function() {
    $scope.mytime = null;
  };
});

要点:

  • ng-model 绑定一个 Date 对象作为时间状态来源;ng-change 在每次合法修改后触发。
  • hour-step、minute-step、show-meridian 均可在控制器里动态切换——示例中的下拉框动态改变步进值,按钮动态切换 12H/24H,这正是下文将讲解的“可配置属性”的典型用法。
  • 通过 mytime | date:'shortTime' 可将模型中的 Date 实时格式化展示,例如 2:40 PM。

三、配置参数全解

以下是 readme 中定义的完整配置项。文档中以 $ 标记的表示该属性支持与作用域表达式绑定(attribute binding);以 C 标记的表示该属性可在模块级配置常量 uibTimepickerConfig 中统一设置;带眼睛图标的表示该属性值会在官方演示中实时体现。

3.1 步进与显示类参数

属性 默认值 说明
hour-step($、C) 1 点击按钮时小时增加或减少的数量
minute-step($、C) 1 点击按钮时分钟增加或减少的数量
second-step($、C) 1 点击按钮时秒增加或减少的数量
show-seconds($、C) false 是否显示秒输入框
show-meridian($、C) true 是否显示 AM/PM,即 12H 模式;设为 false 则进入 24H 模式
show-spinners($、C) true 是否在输入框上下显示加减箭头
pad-hours($) true 小时列是否用 0 填充(如 09 而不是 9)

这些默认值集中定义在 src/timepicker/timepicker.js 的 uibTimepickerConfig 常量中:

.constant('uibTimepickerConfig', {
  hourStep: 1,
  minuteStep: 1,
  secondStep: 1,
  showMeridian: true,
  showSeconds: false,
  meridians: null,
  readonlyInput: false,
  mousewheel: true,
  arrowkeys: true,
  showSpinners: true,
  templateUrl: 'uib/template/timepicker/timepicker.html'
})

从源码看,步进类属性(hourStep 等)一旦在属性上指定,会通过 $scope.$parent.$watch($parse($attrs.hourStep), ...) 建立监听并以 +value 强转为数字(src/timepicker/timepicker.js),因此支持在运行时动态变更步进值。

3.2 交互行为类参数

属性 默认值 说明
arrowkeys($、C) true 是否允许在小时/分钟/秒输入框内使用上下方向键增减数值
mousewheel($、C) true 是否允许在输入框内滚动鼠标滚轮增减数值
readonly-input($、C) false 是否禁止用户手动键入小时/分钟/秒,只能通过按钮、滚轮或方向键操作
ng-disabled($) false 是否整体禁用组件

这些开关在控制器 init 中生效(src/timepicker/timepicker.js):当 mousewheel 为真时注册 setupMousewheelEvents,当 arrowkeys 为真时注册 setupArrowkeyEvents,readonlyInput 则决定 setupInputEvents 是否把 updateHours/updateMinutes/updateSeconds 替换为空操作。

3.3 数值边界与本地化类参数

属性 默认值 说明
min($) undefined 用户可选择的最小时间
max($) undefined 用户可选择的最大时间
meridians($、C) null 基于 locale 的 AM/PM 标签;要覆盖必须提供一个数组,如 ['AM', 'PM']
ng-model($) — 提供时间状态的 Date 对象

min 与 max 通过 new Date(value) 解析(src/timepicker/timepicker.js),若解析结果不是有效日期(isNaN)则视作 undefined 即不限制。meridians 的解析逻辑是:优先取属性上绑定的表达式,其次取 uibTimepickerConfig.meridians,最后回退到 $locale.DATETIME_FORMATS.AMPMS(src/timepicker/timepicker.js)——这意味着不指定时它天然跟随 AngularJS locale 配置。

3.4 模板与 DOM 类参数

属性 默认值 说明
tabindex 0 为时间选择器中的每个控件设置 tabindex
template-url(C) uib/template/timepicker/timepicker.html 覆盖组件使用的模板

tabindex 在控制器初始化时从属性读取,随后从宿主元素上移除原 tabindex 属性(src/timepicker/timepicker.js),再在模板中通过一次性绑定 {{::tabindex}} 应用到各个输入框与 AM/PM 按钮上。template-url 则在指令的 templateUrl 函数中解析:优先取属性值,否则用配置常量(src/timepicker/timepicker.js)。

四、三种增减方式:按钮、滚轮与方向键

控件提供三套并行的增减机制,全部最终汇入统一的 incrementHours/incrementMinutes/incrementSeconds 与 decrementHours/... 方法:

  • 按钮:默认模板 template/timepicker/timepicker.html 渲染三行结构——上行 spinner、中间输入行、下行 spinner,加减箭头是 btn btn-link 链接,点击时调用 incrementHours() 等方法,并通过 ng-class="{disabled: noIncrementHours()}" 与 ng-disabled 在越界时自动置灰。
  • 滚轮:setupMousewheelEvents(src/timepicker/timepicker.js)监听 mousewheel wheel 事件,兼容 wheelDelta 与 deltaY 两种事件模型,向上滚动增、向下滚动减,并在非禁用态下用 $scope.$apply 包装变更。
  • 方向键:setupArrowkeyEvents(src/timepicker/timepicker.js)监听 keydown,e.which === 38(上)增、e.which === 40(下)减,同样先 preventDefault 再触发对应方法。

在 12H 模式下,小时与分钟之间、分钟与秒之间的滚动/按键互不干扰;测试 src/timepicker/test/timepicker.spec.js 通过构造 mousewheel(wheelDelta)与 wheel(deltaY)两种事件对象分别验证了两类滚轮事件的兼容性。

五、12H / 24H 模式与 meridian 切换

show-meridian 控制显示模式:

  • true:12H 模式,额外渲染一个 AM/PM 按钮,点击 toggleMeridian() 在上午/下午之间切换。
  • false:24H 模式,隐藏 AM/PM 列,小时输入直接使用 0–23。

模板中通过 ng-show="showMeridian" 控制 AM/PM 按钮与分隔列的显隐(template/timepicker/timepicker.html)。底层转换在 getHoursFromTemplate 与 updateTemplate 中完成(src/timepicker/timepicker.js):

  • 12H 模式下合法小时范围为 1–12,12 被归一化为 0,当 meridian 是 meridians[1](PM)时再加 12;
  • 渲染回模板时,0 或 12 都显示为 12,其余按 hours % 12 转换;
  • updateTemplate 中还根据 selected.getHours() < 12 决定当前显示 meridians[0](AM)还是 meridians[1](PM)。

meridians 属性允许你自定义标签,例如中文环境下可设为 <a href="https://link.gitcode.com/i/39b13ff6ad1203fb926054592e01da37" target="_blank">'上午', '下午']。测试 [src/timepicker/test/timepicker.spec.js 用 ['am', 'pm'] 覆盖验证了自定义标签的渲染。

六、min / max 边界约束的源码机制

min 与 max 不只是简单的比较:控制器为每种操作都提供了“是否允许”判定函数,例如 noIncrementHours(src/timepicker/timepicker.js):

$scope.noIncrementHours = function() {
  var incrementedSelected = addMinutes(selected, hourStep * 60);
  return disabled || incrementedSelected > max ||
    incrementedSelected < selected && incrementedSelected < min;
};

含义是:当组件禁用、或加步进后超过 max、或加步进后反而早于当前值且早于 min 时,禁止加操作。分钟、秒的增减以及 noToggleMeridian 采用同样的模式,保证任何时刻都不会越过边界。越界时模板中的箭头链接会自动获得 disabled 样式,用户既点不动也滚不动。

addMinutes/addSeconds 的实现也值得注意(src/timepicker/timepicker.js):它在原 Date 的时间分量上做算术,再重新 setHours,从而避免跨午夜或夏令时切换时产生毫秒级的误差——这正是测试中 newTime 工具要 setMilliseconds(0) 的原因(src/timepicker/test/timepicker.spec.js)。

测试 src/timepicker/test/timepicker.spec.js 验证了边界行为:当 min 设为 13:41 而当前为 14:40 时,向下按钮处于 disabled 状态,点击、滚轮、方向键三种方式均无法把时间减到 min 之前。

七、输入校验与 readonly-input

当 readonly-input 为 false(默认)时,用户可以手动键入时间。每次 ng-change 触发对应的 updateHours/updateMinutes/updateSeconds(src/timepicker/timepicker.js),其校验逻辑包括:

  • 小时必须满足当前模式的范围:12H 模式为 1–12,24H 模式为 0–23;分钟、秒必须为 0–59;
  • 任一输入非法或整体时间越出 min/max 时,调用 invalidate 设置 ngModelCtrl.$setValidity('time', false),并分别将 invalidHours/invalidMinutes/invalidSeconds 置为 true,模板据此在对应 td 上追加 has-error 样式类;
  • 合法时调用 refresh(),通过 ngModelCtrl.$setViewValue(new Date(selected)) 把新 Date 写回模型。

输入框失焦(blur)时还会做格式化:若值小于 10 且未被填充,则自动补零(pad 函数,src/timepicker/timepicker.js);若模型已清空则调用 makeValid() 恢复合法状态。ng-model 的渲染入口 render(src/timepicker/timepicker.js)会校验模型值类型,非 Date 会输出错误日志并置 time 无效——ng-model 必须是 Date 对象、毫秒时间戳或可被 new Date() 解析的 RFC2822/ISO 8601 字符串。

当 readonly-input 为 true 时,updateHours/updateMinutes/updateSeconds 被替换为 angular.noop,用户无法键入,只能通过按钮、滚轮、方向键操作;模板中的小时、分钟输入框同时带有 ng-readonly="::readonlyInput"(template/timepicker/timepicker.html)。测试 src/timepicker/test/timepicker.spec.js 断言此时三个输入框均带上 readonly 属性。

八、重要注意事项:日期保留的边界

readme 明确指出:该组件不保证在所有情况下绝对保留日期,强烈建议把“日期”的跟踪封装在独立对象中,不要与 datepicker 共用同一个模型。原因在于夏令时(Daylight Savings)切换等场景需要修改日期才能避免出现无法递增或递减的困境。

此外,如果通过 Date.prototype.setDate 等方式原地修改了模型值,必须先打破引用再赋值,例如:

modelValue = new Date(modelValue);

否则时间选择器不会感知到变更。这是因为控制器在 init 中为 ngModelCtrl.$formatters 注册了 modelValue ? new Date(modelValue) : null 的格式化函数(src/timepicker/timepicker.js),只有引用变化才会重新走渲染管线。

九、模板定制与样式

默认模板位于 template/timepicker/timepicker.html,渲染为一个 uib-timepicker 表格,包含上行 spinner、输入行(小时、分隔冒号、分钟,以及 show-seconds 时追加的秒列)和下行 spinner。你可以:

  • 通过 template-url 属性按实例覆盖模板;
  • 通过模块配置统一替换:angular.module(...).config(['uibTimepickerConfig', function(c) { c.templateUrl = 'my/timepicker.html'; }])。

相关样式集中在 src/timepicker/timepicker.css,仅一条规则:.uib-time input { width: 50px; },保证输入框在窄表格列中整齐排列。若走 src/timepicker/index.js 的打包入口,样式会随模块一并引入;若使用无样式入口 index-nocss.js,则需自行引入 CSS。

结语

uib-timepicker 用 16 个可配置属性覆盖了时间选择控件的绝大多数场景:步进粒度、12H/24H、秒显示、spinner 显隐、读写模式、最小/最大约束、模板定制与 locale 化的 AM/PM 标签。结合 src/timepicker/timepicker.js 的源码与 src/timepicker/test/timepicker.spec.js 的测试用例,你可以放心地把时间选择集成进表单,并清楚知晓它在夏令时等边界场景下的行为限制。

登录后查看全文
bootstrap