In a project, a control is needed to enter minutes and seconds, but after investigating some open source projects, no suitable control is found. There is a similar control TimePicker in the Angular Bootstrap UI, but it doesn't go deep into minutes and seconds precision.
Therefore, it is decided to refer to its source code and implement it yourself.
The final effect is as follows:
First is the definition of the directive:
app.directive('minuteSecondPicker', function() { return { restrict: 'EA', require: ['minuteSecondPicker', '?^ngModel'], controller: 'minuteSecondPickerController', replace: true, scope: { validity: '=' }, templateUrl: 'partials/directives/minuteSecondPicker.html', link: function(scope, element, attrs, ctrls) { var minuteSecondPickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if(ngModelCtrl) { minuteSecondPickerCtrl.init(ngModelCtrl, element.find('input')); } } } };});In the above link function, ctrls is an array: ctrls[0] is the controller instance defined on this directive, and ctrls[1] is ngModelCtrl, that is, the controller instance corresponding to ng-model. This order is actually defined by require: ['minuteSecondPicker', '?^ngModel'].
Note that the first dependency is the name of the directive itself, and the corresponding instance declared by the controller in the directive will be passed in. The second dependency is written a bit strange: "?^ngModel", the meaning of? is that even if the dependency is not found, do not throw an exception, that is, the dependency is an optional option. The meaning of ^ is to find the controller of the parent element.
Then, define some default settings used in this directive and implement them through constant directive:
app.constant('minuteSecondPickerConfig', { minuteStep: 1, secondStep: 1, readonlyInput: false, mousewheel: true});Next is the directive corresponding controller, which is declared as follows:
app.controller('minuteSecondPickerController', ['$scope', '$attrs', '$parse', 'minuteSecondPickerConfig', function($scope, $attrs, $parse, minuteSecondPickerConfig) { ...}]);In the directive link function, the init method of this controller is called:
this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; var minutesInputEl = inputs.eq(0), secondsInputEl = inputs.eq(1); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : minuteSecondPickerConfig.mousewheel; if(mousewheel) { this.setupMousewheelEvents(minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : minuteSecondPickerConfig.readonlyInput; this.setupInputEvents(minutesInputEl, secondsInputEl); };The second parameter accepted by the init method is inputs, and the one passed in the link function is: element.find('input'). So the first input box is used to enter minutes, and the second input box is used to enter seconds.
Then, check whether the mousewheel attribute is overwritten. If there is no overwritten, use the default mousewheel set in constant, and make the relevant settings as follows:
// respond on mousewheel spin this.setupMousewheelEvents = function(minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if(e.originalEvent) { e = e.originalEvent; } // pick correct delta variable depending on event var delta = (e.wheelData) ? e.wheelData : -e.deltaY; return (e.detail || delta > 0); }; minutesInputEl.bind('mousewheel wheel', function(e) { $scope.$apply((isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes()); e.preventDefault(); }); secondsInputEl.bind('mousewheel wheel', function(e) { $scope.$apply((isScrollingUp(e)) ? $scope.incrementSeconds() : $scope.decrementSeconds()); e.preventDefault(); }); };The init method will finally make some settings for the inputs itself:
// respond on direct input this.setupInputEvents = function(minutesInputEl, secondsInputEl) { if($scope.readonlyInput) { $scope.updateMinutes = angular.noop; $scope.updateSeconds = angular.noop; return; } var invalidate = function(invalidMinutes, invalidSeconds) { ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); $scope.validity = false; if(angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; } if(angular.isDefined(invalidSeconds)) { $scope.invalidSeconds = invalidSeconds; } }; $scope.updateMinutes = function() { var minutes = getMinutesFromTemplate(); if(angular.isDefined(minutes)) { selected.minutes = minutes; refresh('m'); } else { invalidate(true); } }; minutesInputEl.bind('blur', function(e) { if(!$scope.invalidMinutes && $scope.minutes < 10) { $scope.$apply(function() { $scope.minutes = pad($scope.minutes); }); } }); $scope.updateSeconds = function() { var seconds = getSecondsFromTemplate(); if(angular.isDefined(seconds)) { selected.seconds = seconds; refresh('s'); } else { invalidate(undefined, true); } }; secondsInputEl.bind('blur', function(e) { if(!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply(function() { $scope.seconds = pad($scope.seconds); }); } }); };In this method, an invalidate function is declared for setting the input illegal, which exposes a validity = false property in scope to give the page a chance to respond appropriately.
If the user uses a variable to represent minuteStep or secondStep, then the corresponding watchers need to be set:
var minuteStep = minuteSecondPickerConfig.minuteStep; if($attrs.minuteStep) { $scope.parent.$watch($parse($attrs.minuteStep), function(value) { minuteStep = parseInt(value, 10); }); } var secondStep = minuteSecondPickerConfig.secondStep; if($attrs.secondStep) { $scope.parent.$watch($parse($attrs.secondStep), function(value) { secondStep = parseInt(value, 10); }); }The complete directive implementation code is as follows:
var app = angular.module("minuteSecondPickerDemo");app.directive('minuteSecondPicker', function() { return { restrict: 'EA', require: ['minuteSecondPicker', '?^ngModel'], controller: 'minuteSecondPickerController', replace: true, scope: { validity: '=' }, templateUrl: 'partials/directives/minuteSecondPicker.html', link: function(scope, element, attrs, ctrls) { var minuteSecondPickerCtrl = ctrls[0], ngModelCtrl = ctrls[1]; if(ngModelCtrl) { minuteSecondPickerCtrl.init(ngModelCtrl, element.find('input')); } } } } };});app.constant('minuteSecondPickerConfig', { minuteStep: 1, secondStep: 1, readonlyInput: false, mousewheel: true});app.controller('minuteSecondPickerController', ['$scope', '$attrs', '$parse', 'minuteSecondPickerConfig', function($scope, $attrs, $parse, minuteSecondPickerConfig) { var selected = { minutes: 0, seconds: 0 }, ngModelCtrl = { $setViewValue: angular.noop }; this.init = function(ngModelCtrl_, inputs) { ngModelCtrl = ngModelCtrl_; ngModelCtrl.$render = this.render; var minutesInputEl = inputs.eq(0), secondsInputEl = inputs.eq(1); var mousewheel = angular.isDefined($attrs.mousewheel) ? $scope.$parent.$eval($attrs.mousewheel) : minuteSecondPickerConfig.mousewheel; if(mousewheel) { this.setupMousewheelEvents(minutesInputEl, secondsInputEl); } $scope.readonlyInput = angular.isDefined($attrs.readonlyInput) ? $scope.$parent.$eval($attrs.readonlyInput) : minuteSecondPickerConfig.readonlyInput; this.setupInputEvents(minutesInputEl, secondsInputEl); }; var minuteStep = minuteSecondPickerConfig.minuteStep; if($attrs.minuteStep) { $scope.parent.$watch($parse($attrs.minuteStep), function(value) { minuteStep = parseInt(value, 10); }); } var secondStep = minuteSecondPickerConfig.secondStep; if($attrs.secondStep) { $scope.parent.$watch($parse($attrs.secondStep), function(value) { secondStep = parseInt(value, 10); }); } // respond on mousewheel spin this.setupMousewheelEvents = function(minutesInputEl, secondsInputEl) { var isScrollingUp = function(e) { if(e.originalEvent) { e = e.originalEvent; } // pick correct delta variable depending on event var delta = (e.wheelData) ? e.wheelData : -e.deltaY; return (e.detail || delta > 0); }; minutesInputEl.bind('mousewheel wheel', function(e) { $scope.$apply((isScrollingUp(e)) ? $scope.incrementMinutes() : $scope.decrementMinutes()); e.preventDefault(); }); secondsInputEl.bind('mousewheel wheel', function(e) { $scope.$apply((isScrollingUp(e)) ? $scope.incrementSeconds() : $scope.decrementSeconds()); e.preventDefault(); }); }; // respond on direct input this.setupInputEvents = function(minutesInputEl, secondsInputEl) { if($scope.readonlyInput) { $scope.updateMinutes = angular.noop; $scope.updateSeconds = angular.noop; return; } var invalidate = function(invalidMinutes, invalidSeconds) { ngModelCtrl.$setViewValue(null); ngModelCtrl.$setValidity('time', false); $scope.validity = false; if(angular.isDefined(invalidMinutes)) { $scope.invalidMinutes = invalidMinutes; } if(angular.isDefined(invalidSeconds)) { $scope.invalidSeconds = invalidSeconds; } }; $scope.updateMinutes = function() { var minutes = getMinutesFromTemplate(); if(angular.isDefined(minutes)) { selected.minutes = minutes; refresh('m'); } else { invalidate(true); } }; minutesInputEl.bind('blur', function(e) { if(!$scope.invalidMinutes && $scope.minutes < 10) { $scope.$apply(function() { $scope.minutes = pad($scope.minutes); }); } }); $scope.updateSeconds = function() { var seconds = getSecondsFromTemplate(); if(angular.isDefined(seconds)) { selected.seconds = seconds; refresh('s'); } else { invalidate(undefined, true); } }; secondsInputEl.bind('blur', function(e) { if(!$scope.invalidSeconds && $scope.seconds < 10) { $scope.$apply(function() { $scope.seconds = pad($scope.seconds); }); } }); }; }; this.render = function() { var time = ngModelCtrl.$modelValue ? { minutes: ngModelCtrl.$modelValue.minutes, seconds: ngModelCtrl.$modelValue.seconds } : null; // adjust the time for invalid value at first time if(time.minutes < 0) { time.minutes = 0; } if(time.seconds < 0) { time.seconds = 0; } var totalSeconds = time.minutes * 60 + time.seconds; time = { minutes: Math.floor(totalSeconds / 60), seconds: totalSeconds % 60 }; if(time) { selected = time; makeValid(); updateTemplate(); } }; // call internally when the model is valid function refresh(keyboardChange) { makeValid(); ngModelCtrl.$setViewValue({ minutes: selected.minutes, seconds: selected.seconds }); updateTemplate(keyboardChange); } function makeValid() { ngModelCtrl.$setValidity('time', true); $scope.validity = true; $scope.invalidMinutes = false; $scope.invalidSeconds = false; } function updateTemplate(keyboardChange) { var minutes = selected.minutes, seconds = selected.seconds; $scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes); $scope.seconds = keyboardChange === 's' ? seconds : pad(seconds); } function pad(value) { return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value; } function getMinutesFromTemplate() { var minutes = parseInt($scope.minutes, 10); return (minutes >= 0) ? minutes : undefined; } function getSecondsFromTemplate() { var seconds = parseInt($scope.seconds, 10); if(seconds >= 60) { seconds = 59; } return (seconds >= 0) ? seconds : undefined; } $scope.incrementMinutes = function() { addSeconds(minuteStep * 60); }; $scope.decrementMinutes = function() { addSeconds(-minuteStep * 60); }; $scope.incrementSeconds = function() { addSeconds(secondStep); }; $scope.decrementSeconds = function() { addSeconds(-secondStep); }; function addSeconds(seconds) { var newSeconds = selected.minutes * 60 + selected.seconds + seconds; if(newSeconds < 0) { newSeconds = 0; } selected = { minutes: Math.floor(newSeconds / 60), seconds: newSeconds % 60 }; refresh(); } $scope.previewTime = function(minutes, seconds) { var totalSeconds = parseInt(minutes, 10) * 60 + parseInt(seconds, 10), hh = pad(Math.floor(totalSeconds / 3600)), mm = pad(minutes % 60), ss = pad(seconds); return hh + ':' + mm + ':' + ss; };}]);Corresponding Template implementation:
<table> <tbody> <tr> <td> <a ng-click="incrementMinutes()"> <span></span> </a> </td> <td></td> <td> <a ng-click="incrementSeconds()"> <span></span> </a> </td> <td></td> </tr> <tr> <td ng-class="{'has-error': invalidMinutes}"> <input type="text" ng-model="minutes" ng-change="updateMinutes()" ng-mousewheel="incrementMinutes()"" ng-readonly="readonlyInput" maxlength="3"> </td> <td>:</td> <td ng-class="{'has-error': invalidSeconds}"> <input type="text" ng-model="seconds" ng-change="updateSeconds()" ng-mousewheel="incrementSeconds()" ng-readonly="readonlyInput" maxlength="2"> <td> <!-- preview column --> <td> <span ng-show="validity"> {{ previewTime(minutes, seconds) }} </span> </td> </tr> <tr> <td> <a ng-click="decrementMinutes()"> <span></span> </a> </td> <td></td> <td> <a ng-click="decrementSeconds()"> <span></span> </a> </td> <td></td> </tr> </tbody></table>Test code (i.e. the source code of the dialog taken in the previous screenshot):
<div> <h3>Highlight on <span>{{ movieName }}</span></h3></div><div> <div> <div id="highlight-start"> <h4>Start Time:</h4> <minute-second-picker ng-model="startTime" validity="startTimeValidity"></minute-second-picker> </div> <div id="highlight-end"> <h4>End Time:</h4> <minute-second-picker ng-model="endTime" validity="endTimeValidity"></minute-second-picker> </div> </div> <div> <div> Tags: </div> <div> <tags model="tags" src="s as s.name for s in sourceTags" options="{ addable: 'true' }"></tags> </div> </div></div><div> <button ng-click="ok()" ng-disabled="!startTimeValidity || !endTimeValidity || durationIncorrect(endTime, startTime)">OK</button> <button ng-click="ok()" ng-disabled="!startTimeValidity || durationIncorrect(endTime, startTime)">OK</button> <button ng-click="cancel()">Cancel</button></div>If you still want to study in depth, you can click here to study and attach 3 exciting topics to you:
Bootstrap learning tutorial
Bootstrap practical tutorial
Bootstrap plug-in usage tutorial
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.