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() {