AngularJS Bootstrap 时间选择器 uib-timepicker 完全指南:配置参数、交互模式与源码原理
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 的测试用例,你可以放心地把时间选择集成进表单,并清楚知晓它在夏令时等边界场景下的行为限制。