diff --git a/misc/demo/assets/demo.css b/misc/demo/assets/demo.css
index 7c58d86d10..ddad9986b5 100644
--- a/misc/demo/assets/demo.css
+++ b/misc/demo/assets/demo.css
@@ -9,6 +9,10 @@ body {
opacity: 0;
}
+.ng-invalid {
+ border: 1px solid red !important;
+}
+
section {
padding-top: 30px;
}
diff --git a/src/timepicker/docs/demo.html b/src/timepicker/docs/demo.html
index 172e4b6888..9fdf7452cb 100644
--- a/src/timepicker/docs/demo.html
+++ b/src/timepicker/docs/demo.html
@@ -1,5 +1,7 @@
-
+
+
+
Time is: {{mytime | date:'shortTime' }}
diff --git a/src/timepicker/docs/demo.js b/src/timepicker/docs/demo.js
index 1c456b855c..478545b5be 100644
--- a/src/timepicker/docs/demo.js
+++ b/src/timepicker/docs/demo.js
@@ -21,6 +21,10 @@ var TimepickerDemoCtrl = function ($scope) {
$scope.mytime = d;
};
+ $scope.changed = function () {
+ console.log('Time changed to: ' + $scope.mytime);
+ };
+
$scope.clear = function() {
$scope.mytime = null;
};
diff --git a/src/timepicker/test/timepicker.spec.js b/src/timepicker/test/timepicker.spec.js
index 4ceeb3c115..797456724b 100644
--- a/src/timepicker/test/timepicker.spec.js
+++ b/src/timepicker/test/timepicker.spec.js
@@ -8,7 +8,7 @@ describe('timepicker directive', function () {
$rootScope = _$rootScope_;
$rootScope.time = newTime(14, 40);
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
}));
@@ -82,6 +82,15 @@ describe('timepicker directive', function () {
expect(getModelState()).toEqual([14, 40]);
});
+ it('has `selected` current time when model is initially cleared', function() {
+ $rootScope.time = null;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+
+ expect($rootScope.time).toBe(null);
+ expect(getTimeState()).not.toEqual(['', '', '']);
+ });
+
it('changes inputs when model changes value', function() {
$rootScope.time = newTime(11, 50);
$rootScope.$digest();
@@ -235,7 +244,7 @@ describe('timepicker directive', function () {
});
it('changes only the time part when minutes change', function() {
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.time = newTime(0, 0);
$rootScope.$digest();
@@ -367,7 +376,7 @@ describe('timepicker directive', function () {
$rootScope.hstep = 2;
$rootScope.mstep = 30;
$rootScope.time = newTime(14, 0);
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -530,7 +539,7 @@ describe('timepicker directive', function () {
beforeEach(function() {
$rootScope.meridian = false;
$rootScope.time = newTime(14, 10);
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
});
@@ -559,6 +568,14 @@ describe('timepicker directive', function () {
expect(getModelState()).toEqual([14, 10]);
expect(getMeridianTd().css('display')).toBe('none');
});
+
+ it('handles correctly initially empty model on parent element', function() {
+ $rootScope.time = null;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+
+ expect($rootScope.time).toBe(null);
+ });
});
describe('setting timepickerConfig steps', function() {
@@ -568,7 +585,7 @@ describe('timepicker directive', function () {
timepickerConfig.hourStep = 2;
timepickerConfig.minuteStep = 10;
timepickerConfig.showMeridian = false;
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(timepickerConfig) {
@@ -614,7 +631,7 @@ describe('timepicker directive', function () {
angular.extend(originalConfig, timepickerConfig);
timepickerConfig.meridians = ['π.μ.', 'μ.μ.'];
timepickerConfig.showMeridian = true;
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
}));
afterEach(inject(function(timepickerConfig) {
@@ -637,10 +654,9 @@ describe('timepicker directive', function () {
});
describe('user input validation', function () {
-
var changeInputValueTo;
- beforeEach(inject(function(_$compile_, _$rootScope_, $sniffer) {
+ beforeEach(inject(function($sniffer) {
changeInputValueTo = function (inputEl, value) {
inputEl.val(value);
inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
@@ -661,7 +677,7 @@ describe('timepicker directive', function () {
expect(getModelState()).toEqual([14, 40]);
});
- it('updates hours & pads on input blur', function() {
+ it('updates hours & pads on input change & pads on blur', function() {
var el = getHoursInputEl();
changeInputValueTo(el, 5);
@@ -673,7 +689,7 @@ describe('timepicker directive', function () {
expect(getModelState()).toEqual([17, 40]);
});
- it('updates minutes & pads on input blur', function() {
+ it('updates minutes & pads on input change & pads on blur', function() {
var el = getMinutesInputEl();
changeInputValueTo(el, 9);
@@ -691,6 +707,7 @@ describe('timepicker directive', function () {
changeInputValueTo(el, 'pizza');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('error')).toBe(true);
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
changeInputValueTo(el, 8);
el.blur();
@@ -698,6 +715,7 @@ describe('timepicker directive', function () {
expect(getTimeState()).toEqual(['08', '40', 'PM']);
expect(getModelState()).toEqual([20, 40]);
expect(el.parent().hasClass('error')).toBe(false);
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('clears model when input minutes is invalid & alerts the UI', function() {
@@ -706,16 +724,18 @@ describe('timepicker directive', function () {
changeInputValueTo(el, 'pizza');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('error')).toBe(true);
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
changeInputValueTo(el, 22);
expect(getTimeState()).toEqual(['02', '22', 'PM']);
expect(getModelState()).toEqual([14, 22]);
expect(el.parent().hasClass('error')).toBe(false);
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
});
it('handles 12/24H mode change', function() {
$rootScope.meridian = true;
- element = $compile('
')($rootScope);
+ element = $compile('
')($rootScope);
$rootScope.$digest();
var el = getHoursInputEl();
@@ -723,11 +743,111 @@ describe('timepicker directive', function () {
changeInputValueTo(el, '16');
expect($rootScope.time).toBe(null);
expect(el.parent().hasClass('error')).toBe(true);
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
$rootScope.meridian = false;
$rootScope.$digest();
expect(getTimeState(true)).toEqual(['16', '40']);
expect(getModelState()).toEqual([16, 40]);
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ });
+ });
+
+ describe('when model is not a Date', function() {
+ beforeEach(inject(function() {
+ eelement = $compile('
')($rootScope);
+ }));
+
+ it('should not be invalid when the model is null', function() {
+ $rootScope.time = null;
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ });
+
+ it('should not be invalid when the model is undefined', function() {
+ $rootScope.time = undefined;
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ });
+
+ it('should not be invalid when the model is a valid string date representation', function() {
+ $rootScope.time = 'September 30, 2010 15:30:00';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ expect(getTimeState()).toEqual(['03', '30', 'PM']);
+ });
+
+ it('should be invalid when the model is not a valid string date representation', function() {
+ $rootScope.time = 'pizza';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
+ });
+
+ it('should return valid when the model becomes valid', function() {
+ $rootScope.time = 'pizza';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
+
+ $rootScope.time = new Date();
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ });
+
+ it('should return valid when the model is cleared', function() {
+ $rootScope.time = 'pizza';
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(true);
+
+ $rootScope.time = null;
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid-time')).toBe(false);
+ });
+ });
+
+ describe('use with `ng-required` directive', function() {
+ beforeEach(inject(function() {
+ $rootScope.time = null;
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('should be invalid initially', function() {
+ expect(element.hasClass('ng-invalid')).toBe(true);
+ });
+
+ it('should be valid if model has been specified', function() {
+ $rootScope.time = new Date();
+ $rootScope.$digest();
+ expect(element.hasClass('ng-invalid')).toBe(false);
+ });
+ });
+
+ describe('use with `ng-change` directive', function() {
+ beforeEach(inject(function() {
+ $rootScope.changeHandler = jasmine.createSpy('changeHandler');
+ $rootScope.time = new Date();
+ element = $compile('
')($rootScope);
+ $rootScope.$digest();
+ }));
+
+ it('should not be called initially', function() {
+ expect($rootScope.changeHandler).not.toHaveBeenCalled();
+ });
+
+ it('should be called when hours / minutes buttons clicked', function() {
+ var btn1 = getHoursButton(true);
+ var btn2 = getMinutesButton(false);
+
+ doClick(btn1, 2);
+ doClick(btn2, 3);
+ $rootScope.$digest();
+ expect($rootScope.changeHandler.callCount).toBe(5);
+ });
+
+ it('should not be called when model changes programatically', function() {
+ $rootScope.time = new Date();
+ $rootScope.$digest();
+ expect($rootScope.changeHandler).not.toHaveBeenCalled();
});
});
diff --git a/src/timepicker/timepicker.js b/src/timepicker/timepicker.js
index db65378f68..3e6f9064f6 100644
--- a/src/timepicker/timepicker.js
+++ b/src/timepicker/timepicker.js
@@ -1,14 +1,5 @@
angular.module('ui.bootstrap.timepicker', [])
-.filter('pad', function() {
- return function(input) {
- if ( angular.isDefined(input) && input.toString().length < 2 ) {
- input = '0' + input;
- }
- return input;
- };
-})
-
.constant('timepickerConfig', {
hourStep: 1,
minuteStep: 1,
@@ -18,16 +9,18 @@ angular.module('ui.bootstrap.timepicker', [])
mousewheel: true
})
-.directive('timepicker', ['padFilter', '$parse', 'timepickerConfig', function (padFilter, $parse, timepickerConfig) {
+.directive('timepicker', ['$parse', '$log', 'timepickerConfig', function ($parse, $log, timepickerConfig) {
return {
restrict: 'EA',
- require:'ngModel',
+ require:'?^ngModel',
replace: true,
+ scope: {},
templateUrl: 'template/timepicker/timepicker.html',
- scope: {
- model: '=ngModel'
- },
- link: function(scope, element, attrs, ngModelCtrl) {
+ link: function(scope, element, attrs, ngModel) {
+ if ( !ngModel ) {
+ return; // do nothing if no ng-model
+ }
+
var selected = new Date(), meridians = timepickerConfig.meridians;
var hourStep = timepickerConfig.hourStep;
@@ -48,28 +41,27 @@ angular.module('ui.bootstrap.timepicker', [])
scope.showMeridian = timepickerConfig.showMeridian;
if (attrs.showMeridian) {
scope.$parent.$watch($parse(attrs.showMeridian), function(value) {
- scope.showMeridian = !! value;
-
- if ( ! scope.model ) {
- // Reset
- var dt = new Date( selected );
- var hours = getScopeHours();
- if (angular.isDefined( hours )) {
- dt.setHours( hours );
+ scope.showMeridian = !!value;
+
+ if ( ngModel.$error.time ) {
+ // Evaluate from template
+ var hours = getHoursFromTemplate(), minutes = getMinutesFromTemplate();
+ if (angular.isDefined( hours ) && angular.isDefined( minutes )) {
+ selected.setHours( hours );
+ refresh();
}
- scope.model = new Date( dt );
} else {
- refreshTemplate();
+ updateTemplate();
}
});
}
// Get scope.hours in 24H mode if valid
- function getScopeHours ( ) {
+ function getHoursFromTemplate ( ) {
var hours = parseInt( scope.hours, 10 );
var valid = ( scope.showMeridian ) ? (hours > 0 && hours < 13) : (hours >= 0 && hours < 24);
if ( !valid ) {
- return;
+ return undefined;
}
if ( scope.showMeridian ) {
@@ -83,14 +75,22 @@ angular.module('ui.bootstrap.timepicker', [])
return hours;
}
+ function getMinutesFromTemplate() {
+ var minutes = parseInt(scope.minutes, 10);
+ return ( minutes >= 0 && minutes < 60 ) ? minutes : undefined;
+ }
+
+ function pad( value ) {
+ return ( angular.isDefined(value) && value.toString().length < 2 ) ? '0' + value : value;
+ }
+
// Input elements
- var inputs = element.find('input');
- var hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);
+ var inputs = element.find('input'), hoursInputEl = inputs.eq(0), minutesInputEl = inputs.eq(1);
// Respond on mousewheel spin
var mousewheel = (angular.isDefined(attrs.mousewheel)) ? scope.$eval(attrs.mousewheel) : timepickerConfig.mousewheel;
if ( mousewheel ) {
-
+
var isScrollingUp = function(e) {
if (e.originalEvent) {
e = e.originalEvent;
@@ -99,7 +99,7 @@ angular.module('ui.bootstrap.timepicker', [])
var delta = (e.wheelDelta) ? e.wheelDelta : -e.deltaY;
return (e.detail || delta > 0);
};
-
+
hoursInputEl.bind('mousewheel wheel', function(e) {
scope.$apply( (isScrollingUp(e)) ? scope.incrementHours() : scope.decrementHours() );
e.preventDefault();
@@ -111,50 +111,54 @@ angular.module('ui.bootstrap.timepicker', [])
});
}
- var keyboardChange = false;
scope.readonlyInput = (angular.isDefined(attrs.readonlyInput)) ? scope.$eval(attrs.readonlyInput) : timepickerConfig.readonlyInput;
if ( ! scope.readonlyInput ) {
+
+ var invalidate = function(invalidHours, invalidMinutes) {
+ ngModel.$setViewValue( null );
+ ngModel.$setValidity('time', false);
+ if (angular.isDefined(invalidHours)) {
+ scope.invalidHours = invalidHours;
+ }
+ if (angular.isDefined(invalidMinutes)) {
+ scope.invalidMinutes = invalidMinutes;
+ }
+ };
+
scope.updateHours = function() {
- var hours = getScopeHours();
+ var hours = getHoursFromTemplate();
if ( angular.isDefined(hours) ) {
- keyboardChange = 'h';
- if ( scope.model === null ) {
- scope.model = new Date( selected );
- }
- scope.model.setHours( hours );
+ selected.setHours( hours );
+ refresh( 'h' );
} else {
- scope.model = null;
- scope.validHours = false;
+ invalidate(true);
}
};
hoursInputEl.bind('blur', function(e) {
- if ( scope.validHours && scope.hours < 10) {
+ if ( !scope.validHours && scope.hours < 10) {
scope.$apply( function() {
- scope.hours = padFilter( scope.hours );
+ scope.hours = pad( scope.hours );
});
}
});
scope.updateMinutes = function() {
- var minutes = parseInt(scope.minutes, 10);
- if ( minutes >= 0 && minutes < 60 ) {
- keyboardChange = 'm';
- if ( scope.model === null ) {
- scope.model = new Date( selected );
- }
- scope.model.setMinutes( minutes );
+ var minutes = getMinutesFromTemplate();
+
+ if ( angular.isDefined(minutes) ) {
+ selected.setMinutes( minutes );
+ refresh( 'm' );
} else {
- scope.model = null;
- scope.validMinutes = false;
+ invalidate(undefined, true);
}
};
minutesInputEl.bind('blur', function(e) {
- if ( scope.validMinutes && scope.minutes < 10 ) {
+ if ( !scope.invalidMinutes && scope.minutes < 10 ) {
scope.$apply( function() {
- scope.minutes = padFilter( scope.minutes );
+ scope.minutes = pad( scope.minutes );
});
}
});
@@ -163,38 +167,49 @@ angular.module('ui.bootstrap.timepicker', [])
scope.updateMinutes = angular.noop;
}
- scope.$watch( function getModelTimestamp() {
- return +scope.model;
- }, function( timestamp ) {
- if ( !isNaN( timestamp ) && timestamp > 0 ) {
- selected = new Date( timestamp );
- refreshTemplate();
- }
- });
+ ngModel.$render = function() {
+ var date = ngModel.$modelValue ? new Date( ngModel.$modelValue ) : null;
- function refreshTemplate() {
- var hours = selected.getHours();
- if ( scope.showMeridian ) {
- // Convert 24 to 12 hour system
- hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12;
+ if ( isNaN(date) ) {
+ ngModel.$setValidity('time', false);
+ $log.error('Timepicker directive: "ng-model" value must be a Date object, a number of milliseconds since 01.01.1970 or a string representing an RFC2822 or ISO 8601 date.');
+ } else {
+ if ( date ) {
+ selected = date;
+ }
+ makeValid();
+ updateTemplate();
}
- scope.hours = ( keyboardChange === 'h' ) ? hours : padFilter(hours);
- scope.validHours = true;
+ };
- var minutes = selected.getMinutes();
- scope.minutes = ( keyboardChange === 'm' ) ? minutes : padFilter(minutes);
- scope.validMinutes = true;
+ // Call internally when we know that model is valid.
+ function refresh( keyboardChange ) {
+ makeValid();
+ ngModel.$setViewValue( new Date(selected) );
+ updateTemplate( keyboardChange );
+ }
- scope.meridian = ( scope.showMeridian ) ? (( selected.getHours() < 12 ) ? meridians[0] : meridians[1]) : '';
+ function makeValid() {
+ ngModel.$setValidity('time', true);
+ scope.invalidHours = false;
+ scope.invalidMinutes = false;
+ }
- keyboardChange = false;
+ function updateTemplate( keyboardChange ) {
+ var hours = selected.getHours(), minutes = selected.getMinutes();
+
+ if ( scope.showMeridian ) {
+ hours = ( hours === 0 || hours === 12 ) ? 12 : hours % 12; // Convert 24 to 12 hour system
+ }
+ scope.hours = keyboardChange === 'h' ? hours : pad(hours);
+ scope.minutes = keyboardChange === 'm' ? minutes : pad(minutes);
+ scope.meridian = selected.getHours() < 12 ? meridians[0] : meridians[1];
}
function addMinutes( minutes ) {
var dt = new Date( selected.getTime() + minutes * 60000 );
- selected.setHours( dt.getHours() );
- selected.setMinutes( dt.getMinutes() );
- scope.model = new Date( selected );
+ selected.setHours( dt.getHours(), dt.getMinutes() );
+ refresh();
}
scope.incrementHours = function() {
diff --git a/template/timepicker/timepicker.html b/template/timepicker/timepicker.html
index 5ef9d0083a..56ac1b5ad7 100644
--- a/template/timepicker/timepicker.html
+++ b/template/timepicker/timepicker.html
@@ -6,9 +6,9 @@
|
- |
+ |
: |
- |
+ |
|