From 581156083f186498445851ee752fa3156b155953 Mon Sep 17 00:00:00 2001 From: Shahar Talmi <shahar.talmi@gmail.com> Date: Mon, 1 Sep 2014 22:25:43 +0300 Subject: [PATCH 1/2] fix(ngModel): support milliseconds in time and datetime Closes #8874 --- src/ng/directive/input.js | 16 +++++----- test/ng/directive/inputSpec.js | 56 +++++++++++++++++++++++++--------- 2 files changed, 50 insertions(+), 22 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 1692061a0146..a1de53bebb41 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -14,10 +14,10 @@ var URL_REGEXP = /^(ftp|http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\ var EMAIL_REGEXP = /^[a-z0-9!#$%&'*+\/=?^_`{|}~.-]+@[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i; var NUMBER_REGEXP = /^\s*(\-|\+)?(\d+|(\d*(\.\d*)))\s*$/; var DATE_REGEXP = /^(\d{4})-(\d{2})-(\d{2})$/; -var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d))?$/; +var DATETIMELOCAL_REGEXP = /^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var WEEK_REGEXP = /^(\d{4})-W(\d\d)$/; var MONTH_REGEXP = /^(\d{4})-(\d\d)$/; -var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d))?$/; +var TIME_REGEXP = /^(\d\d):(\d\d)(?::(\d\d)(\.\d{1,3})?)?$/; var DEFAULT_REGEXP = /(\s+|^)default(\s+|$)/; var $ngModelMinErr = new minErr('ngModel'); @@ -281,8 +281,8 @@ var inputType = { </example> */ 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, - createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss']), - 'yyyy-MM-ddTHH:mm:ss'), + createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), + 'yyyy-MM-ddTHH:mm:ss.sss'), /** * @ngdoc input @@ -370,8 +370,8 @@ var inputType = { </example> */ 'time': createDateInputType('time', TIME_REGEXP, - createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss']), - 'HH:mm:ss'), + createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), + 'HH:mm:ss.sss'), /** * @ngdoc input @@ -1067,7 +1067,7 @@ function createDateParser(regexp, mapping) { HH: date.getHours(), mm: date.getMinutes(), ss: date.getSeconds(), - sss: date.getMilliseconds() + sss: date.getMilliseconds() / 1000 }; } else { map = { yyyy: 1970, MM: 1, dd: 1, HH: 0, mm: 0, ss: 0, sss: 0 }; @@ -1078,7 +1078,7 @@ function createDateParser(regexp, mapping) { map[mapping[index]] = +part; } }); - return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss || 0); + return new Date(map.yyyy, map.MM - 1, map.dd, map.HH, map.mm, map.ss || 0, map.sss * 1000 || 0); } } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index a50c1640cfc5..7d396f38ef41 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2590,13 +2590,13 @@ describe('input', function() { }); it('should set the view if the model if a valid Date object.', function(){ - compileInput('<input type="datetime-local" ng-model="tenSecondsToNextYear"/>'); + compileInput('<input type="datetime-local" ng-model="halfSecondToNextYear"/>'); scope.$apply(function (){ - scope.tenSecondsToNextYear = new Date(2013, 11, 31, 23, 59, 0); + scope.halfSecondToNextYear = new Date(2013, 11, 31, 23, 59, 59, 500); }); - expect(inputElm.val()).toBe('2013-12-31T23:59:00'); + expect(inputElm.val()).toBe('2013-12-31T23:59:59.500'); }); it('should set the model undefined if the view is invalid', function (){ @@ -2606,7 +2606,7 @@ describe('input', function() { scope.breakMe = new Date(2009, 0, 6, 16, 25, 0); }); - expect(inputElm.val()).toBe('2009-01-06T16:25:00'); + expect(inputElm.val()).toBe('2009-01-06T16:25:00.000'); try { //set to text for browsers with datetime-local validation. @@ -2663,7 +2663,21 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0)); }); - expect(inputElm.val()).toBe('2001-01-01T01:02:00'); + expect(inputElm.val()).toBe('2001-01-01T01:02:00.000'); + }); + + it('should allow to specify the milliseconds', function() { + compileInput('<input type="datetime-local" ng-model="value"" />'); + + changeInputValueTo('2000-01-01T01:02:03.500'); + expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3, 500)); + }); + + it('should allow to specify single digit milliseconds', function() { + compileInput('<input type="datetime-local" ng-model="value"" />'); + + changeInputValueTo('2000-01-01T01:02:03.4'); + expect(+scope.value).toBe(+new Date(2000, 0, 1, 1, 2, 3, 400)); }); it('should allow to specify the seconds', function() { @@ -2675,7 +2689,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(2001, 0, 1, 1, 2, 3); }); - expect(inputElm.val()).toBe('2001-01-01T01:02:03'); + expect(inputElm.val()).toBe('2001-01-01T01:02:03.000'); }); it('should allow to skip the seconds', function() { @@ -2854,10 +2868,10 @@ describe('input', function() { compileInput('<input type="time" ng-model="threeFortyOnePm"/>'); scope.$apply(function (){ - scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0); + scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500); }); - expect(inputElm.val()).toBe('15:41:00'); + expect(inputElm.val()).toBe('15:41:00.500'); }); it('should set the model undefined if the view is invalid', function (){ @@ -2867,7 +2881,7 @@ describe('input', function() { scope.breakMe = new Date(1970, 0, 1, 16, 25, 0); }); - expect(inputElm.val()).toBe('16:25:00'); + expect(inputElm.val()).toBe('16:25:00.000'); try { //set to text for browsers with time validation. @@ -2924,7 +2938,21 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0)); }); - expect(inputElm.val()).toBe('23:02:00'); + expect(inputElm.val()).toBe('23:02:00.000'); + }); + + it('should allow to specify the milliseconds', function() { + compileInput('<input type="time" ng-model="value"" />'); + + changeInputValueTo('01:02:03.500'); + expect(+scope.value).toBe(+new Date(1970, 0, 1, 1, 2, 3, 500)); + }); + + it('should allow to specify single digit milliseconds', function() { + compileInput('<input type="time" ng-model="value"" />'); + + changeInputValueTo('01:02:03.4'); + expect(+scope.value).toBe(+new Date(1970, 0, 1, 1, 2, 3, 400)); }); it('should allow to specify the seconds', function() { @@ -2936,7 +2964,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(1970, 0, 1, 1, 2, 3); }); - expect(inputElm.val()).toBe('01:02:03'); + expect(inputElm.val()).toBe('01:02:03.000'); }); it('should allow to skip the seconds', function() { @@ -3192,13 +3220,13 @@ describe('input', function() { scope.val = new Date(2013, 1, 2, 3, 4, 5, 6); }); - expect(timeElm.val()).toBe('03:04:05'); + expect(timeElm.val()).toBe('03:04:05.006'); expect(monthElm.val()).toBe('2013-02'); expect(weekElm.val()).toBe('2013-W05'); changeGivenInputTo(monthElm, '2012-02'); expect(monthElm.val()).toBe('2012-02'); - expect(timeElm.val()).toBe('03:04:05'); + expect(timeElm.val()).toBe('03:04:05.006'); expect(weekElm.val()).toBe('2012-W05'); changeGivenInputTo(timeElm, '04:05:06'); @@ -3211,7 +3239,7 @@ describe('input', function() { expect(timeElm.val()).toBe('04:05:06'); expect(weekElm.val()).toBe('2014-W01'); - expect(+scope.val).toBe(+new Date(2014, 0, 2, 4, 5, 6, 6)); + expect(+scope.val).toBe(+new Date(2014, 0, 2, 4, 5, 6, 0)); function changeGivenInputTo(inputElm, value) { inputElm.val(value); From d60687a4d030ef667e7ac0e6d0c6271dd806b1a7 Mon Sep 17 00:00:00 2001 From: Shahar Talmi <shahar.talmi@gmail.com> Date: Thu, 4 Sep 2014 13:31:16 +0300 Subject: [PATCH 2/2] fix(ngModel): do not display milliseconds unless required 1) milliseconds count is non-zero 2) step attribute is less than one --- src/ng/directive/input.js | 19 +++++++++++-- test/ng/directive/inputSpec.js | 52 ++++++++++++++++++++++++++++++---- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index a1de53bebb41..3de490f8ba5e 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -282,7 +282,13 @@ var inputType = { */ 'datetime-local': createDateInputType('datetimelocal', DATETIMELOCAL_REGEXP, createDateParser(DATETIMELOCAL_REGEXP, ['yyyy', 'MM', 'dd', 'HH', 'mm', 'ss', 'sss']), - 'yyyy-MM-ddTHH:mm:ss.sss'), + function(value, step) { + if (value.getMilliseconds() || (step && parseFloat(step) < 1)) { + return 'yyyy-MM-ddTHH:mm:ss.sss'; + } else { + return 'yyyy-MM-ddTHH:mm:ss'; + } + }), /** * @ngdoc input @@ -371,7 +377,13 @@ var inputType = { */ 'time': createDateInputType('time', TIME_REGEXP, createDateParser(TIME_REGEXP, ['HH', 'mm', 'ss', 'sss']), - 'HH:mm:ss.sss'), + function(value, step) { + if (value.getMilliseconds() || (step && parseFloat(step) < 1)) { + return 'HH:mm:ss.sss'; + } else { + return 'HH:mm:ss'; + } + }), /** * @ngdoc input @@ -1110,9 +1122,10 @@ function createDateInputType(type, regexp, parseDate, format) { return undefined; }); + var invokeFormat = isFunction(format); ctrl.$formatters.push(function(value) { if (isDate(value)) { - return $filter('date')(value, format, timezone); + return $filter('date')(value, invokeFormat ? format(value, attr.step) : format, timezone); } return ''; }); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 7d396f38ef41..2f8c8794fba1 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -2590,6 +2590,16 @@ describe('input', function() { }); it('should set the view if the model if a valid Date object.', function(){ + compileInput('<input type="datetime-local" ng-model="oneSecondToNextYear"/>'); + + scope.$apply(function (){ + scope.oneSecondToNextYear = new Date(2013, 11, 31, 23, 59, 59); + }); + + expect(inputElm.val()).toBe('2013-12-31T23:59:59'); + }); + + it('should set the view with milliseconds', function(){ compileInput('<input type="datetime-local" ng-model="halfSecondToNextYear"/>'); scope.$apply(function (){ @@ -2599,6 +2609,16 @@ describe('input', function() { expect(inputElm.val()).toBe('2013-12-31T23:59:59.500'); }); + it('should set the view with milliseconds if step < 1', function(){ + compileInput('<input step="0.1" type="datetime-local" ng-model="halfSecondToNextYear"/>'); + + scope.$apply(function (){ + scope.halfSecondToNextYear = new Date(2013, 11, 31, 23, 59, 59); + }); + + expect(inputElm.val()).toBe('2013-12-31T23:59:59.000'); + }); + it('should set the model undefined if the view is invalid', function (){ compileInput('<input type="datetime-local" ng-model="breakMe"/>'); @@ -2606,7 +2626,7 @@ describe('input', function() { scope.breakMe = new Date(2009, 0, 6, 16, 25, 0); }); - expect(inputElm.val()).toBe('2009-01-06T16:25:00.000'); + expect(inputElm.val()).toBe('2009-01-06T16:25:00'); try { //set to text for browsers with datetime-local validation. @@ -2663,7 +2683,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0)); }); - expect(inputElm.val()).toBe('2001-01-01T01:02:00.000'); + expect(inputElm.val()).toBe('2001-01-01T01:02:00'); }); it('should allow to specify the milliseconds', function() { @@ -2689,7 +2709,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(2001, 0, 1, 1, 2, 3); }); - expect(inputElm.val()).toBe('2001-01-01T01:02:03.000'); + expect(inputElm.val()).toBe('2001-01-01T01:02:03'); }); it('should allow to skip the seconds', function() { @@ -2867,6 +2887,16 @@ describe('input', function() { it('should set the view if the model if a valid Date object.', function(){ compileInput('<input type="time" ng-model="threeFortyOnePm"/>'); + scope.$apply(function (){ + scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0); + }); + + expect(inputElm.val()).toBe('15:41:00'); + }); + + it('should set the view with milliseconds', function(){ + compileInput('<input type="time" ng-model="threeFortyOnePm"/>'); + scope.$apply(function (){ scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0, 500); }); @@ -2874,6 +2904,16 @@ describe('input', function() { expect(inputElm.val()).toBe('15:41:00.500'); }); + it('should set the view with milliseconds if step < 1', function(){ + compileInput('<input step="0.1" type="time" ng-model="threeFortyOnePm"/>'); + + scope.$apply(function (){ + scope.threeFortyOnePm = new Date(1970, 0, 1, 15, 41, 0); + }); + + expect(inputElm.val()).toBe('15:41:00.000'); + }); + it('should set the model undefined if the view is invalid', function (){ compileInput('<input type="time" ng-model="breakMe"/>'); @@ -2881,7 +2921,7 @@ describe('input', function() { scope.breakMe = new Date(1970, 0, 1, 16, 25, 0); }); - expect(inputElm.val()).toBe('16:25:00.000'); + expect(inputElm.val()).toBe('16:25:00'); try { //set to text for browsers with time validation. @@ -2938,7 +2978,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0)); }); - expect(inputElm.val()).toBe('23:02:00.000'); + expect(inputElm.val()).toBe('23:02:00'); }); it('should allow to specify the milliseconds', function() { @@ -2964,7 +3004,7 @@ describe('input', function() { scope.$apply(function() { scope.value = new Date(1970, 0, 1, 1, 2, 3); }); - expect(inputElm.val()).toBe('01:02:03.000'); + expect(inputElm.val()).toBe('01:02:03'); }); it('should allow to skip the seconds', function() {