From 6b21e54d1e0a28e270b29b6e59833e311840e018 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 20 Nov 2017 10:39:38 +0100 Subject: [PATCH 1/3] fix(input): allow overriding timezone for date input types This commit also fixes a bug where part of the Date object was re-used even after the input was emptied. Fixes #16181 Closes #13382 --- src/ng/directive/input.js | 9 ++- test/ng/directive/inputSpec.js | 110 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 2 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 8a07e5e6b37f..f7970e924313 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1468,11 +1468,13 @@ function createDateInputType(type, regexp, parseDate, format) { return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) { badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); - var timezone = ctrl && ctrl.$options.getOption('timezone'); var previousDate; ctrl.$parsers.push(function(value) { - if (ctrl.$isEmpty(value)) return null; + if (ctrl.$isEmpty(value)) { + previousDate = null; + return null; + } if (regexp.test(value)) { // Note: We cannot read ctrl.$modelValue, as there might be a different // parser/formatter in the processing chain so that the model @@ -1489,6 +1491,7 @@ function createDateInputType(type, regexp, parseDate, format) { } if (isValidDate(value)) { previousDate = value; + var timezone = ctrl.$options.getOption('timezone'); if (previousDate && timezone) { previousDate = convertTimezoneToLocal(previousDate, timezone, true); } @@ -1532,6 +1535,8 @@ function createDateInputType(type, regexp, parseDate, format) { function parseDateAndConvertTimeZoneToLocal(value, previousDate) { var parsedDate = parseDate(value, previousDate); + var timezone = ctrl.$options.getOption('timezone'); + if (!isNaN(parsedDate) && timezone) { parsedDate = convertTimezoneToLocal(parsedDate, timezone); } diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index cb87957b342c..efc85ecaecbf 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -718,6 +718,21 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2013-07'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 6, 1)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2014, 6, 1)); + }); + expect(inputElm.val()).toBe('2014-06'); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1004,6 +1019,21 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2013-W03'); + expect(+$rootScope.value).toBe(Date.UTC(2013, 0, 17)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '+5000'}); + + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2014, 0, 17)); + }); + expect(inputElm.val()).toBe('2014-W04'); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1229,6 +1259,25 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01T01:02'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '+0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0)); + }); + expect(inputElm.val()).toBe('2001-01-01T06:02:00.000'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + + helper.changeInputValueTo('2000-01-01T01:02'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1591,6 +1640,25 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('23:02:00'); + expect(+$rootScope.value).toBe(Date.UTC(1970, 0, 1, 23, 2, 0)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0)); + }); + expect(inputElm.val()).toBe('18:02:00.000'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + helper.changeInputValueTo('23:02:00'); + // The year is still set from the previous date + expect(+$rootScope.value).toBe(Date.UTC(1971, 0, 1, 23, 2, 0)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -1920,6 +1988,24 @@ describe('input', function() { }); + it('should be possible to override the timezone', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1)); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2001, 0, 1)); + }); + expect(inputElm.val()).toBe('2000-12-31'); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 19)); + }); + + they('should use any timezone if specified in the options (format: $prop)', {'+HHmm': '+0500', '+HH:mm': '+05:00'}, function(tz) { @@ -2005,6 +2091,30 @@ describe('input', function() { dealoc(formElm); }); + it('should not reuse the hour part of a previous date object after emptying the input', function() { + var inputElm = helper.compileInput(''); + + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1)); + + // Change the timezone offset so that the display date is a day earlier + // This does not change the model, but our implementation + // internally caches a Date object with this offset + // and re-uses it if part of the date changes + inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2001, 0, 1)); + }); + expect(inputElm.val()).toBe('2000-12-31'); + + // Emptying the input should clear the cached date object + helper.changeInputValueTo(''); + + inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + helper.changeInputValueTo('2000-01-01'); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); + }); + describe('min', function() { it('should invalidate', function() { From 9ef6897a717ea6044efbad03690865d6c03eca29 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Thu, 23 Nov 2017 11:40:27 +0100 Subject: [PATCH 2/3] fixup! remove incorrect date reset bugfix --- src/.eslintrc.json | 1 + src/Angular.js | 1 + src/ng/directive/input.js | 17 ++++++++++++----- src/ng/directive/ngModelOptions.js | 2 ++ test/ng/directive/inputSpec.js | 19 ++++++++++--------- 5 files changed, 26 insertions(+), 14 deletions(-) diff --git a/src/.eslintrc.json b/src/.eslintrc.json index e053a4dfb972..6cfa489bf0d0 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -79,6 +79,7 @@ "toJsonReplacer": false, "toJson": false, "fromJson": false, + "addDateMinutes": false, "convertTimezoneToLocal": false, "timezoneToOffset": false, "startingTag": false, diff --git a/src/Angular.js b/src/Angular.js index 7eda3a028b6b..38c564f62df5 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -75,6 +75,7 @@ fromJson, convertTimezoneToLocal, timezoneToOffset, + addDateMinutes, startingTag, tryDecodeURIComponent, parseKeyValue, diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index f7970e924313..4ec6ba26971a 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1469,12 +1469,11 @@ function createDateInputType(type, regexp, parseDate, format) { badInputChecker(scope, element, attr, ctrl, type); baseInputType(scope, element, attr, ctrl, $sniffer, $browser); var previousDate; + var previousTimezone; ctrl.$parsers.push(function(value) { - if (ctrl.$isEmpty(value)) { - previousDate = null; - return null; - } + if (ctrl.$isEmpty(value)) return null; + if (regexp.test(value)) { // Note: We cannot read ctrl.$modelValue, as there might be a different // parser/formatter in the processing chain so that the model @@ -1492,12 +1491,14 @@ function createDateInputType(type, regexp, parseDate, format) { if (isValidDate(value)) { previousDate = value; var timezone = ctrl.$options.getOption('timezone'); - if (previousDate && timezone) { + if (timezone) { + previousTimezone = timezone; previousDate = convertTimezoneToLocal(previousDate, timezone, true); } return $filter('date')(value, format, timezone); } else { previousDate = null; + previousTimezone = null; return ''; } }); @@ -1534,6 +1535,12 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseDateAndConvertTimeZoneToLocal(value, previousDate) { + if (timezone && previousTimezone && previousTimezone !== timezone) { + // If the timezone has changed, adjust the previousDate to the default timzeone + // so that the new date is converted with the correct timezone offset + previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone, 0)); + } + var parsedDate = parseDate(value, previousDate); var timezone = ctrl.$options.getOption('timezone'); diff --git a/src/ng/directive/ngModelOptions.js b/src/ng/directive/ngModelOptions.js index 02a328ff3748..62408d2ea84a 100644 --- a/src/ng/directive/ngModelOptions.js +++ b/src/ng/directive/ngModelOptions.js @@ -462,6 +462,8 @@ defaultModelOptions = new ModelOptions({ * continental US time zone abbreviations, but for general use, use a time zone offset, for * example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian) * If not specified, the timezone of the browser will be used. + * Note that changing the timezone will have no effect on the current date, and is only applied after + * the next input / model change. * */ var ngModelOptionsDirective = function() { diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index efc85ecaecbf..104c2df5d2cb 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -727,9 +727,9 @@ describe('input', function() { inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(2014, 6, 1)); + $rootScope.value = new Date(Date.UTC(2013, 6, 1)); }); - expect(inputElm.val()).toBe('2014-06'); + expect(inputElm.val()).toBe('2013-06'); }); @@ -1028,9 +1028,10 @@ describe('input', function() { inputElm.controller('ngModel').$overrideModelOptions({timezone: '+5000'}); $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(2014, 0, 17)); + // the 17. with an offset of +5000 moves the date into next week + $rootScope.value = new Date(Date.UTC(2013, 0, 18)); }); - expect(inputElm.val()).toBe('2014-W04'); + expect(inputElm.val()).toBe('2013-W04'); }); @@ -2002,7 +2003,7 @@ describe('input', function() { inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); helper.changeInputValueTo('2000-01-01'); - expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 19)); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); }); @@ -2091,11 +2092,11 @@ describe('input', function() { dealoc(formElm); }); - it('should not reuse the hour part of a previous date object after emptying the input', function() { + it('should not reuse the hour part of a previous date object after changing the timezone', function() { var inputElm = helper.compileInput(''); helper.changeInputValueTo('2000-01-01'); - expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1)); + expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); // Change the timezone offset so that the display date is a day earlier // This does not change the model, but our implementation @@ -2103,9 +2104,9 @@ describe('input', function() { // and re-uses it if part of the date changes inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); $rootScope.$apply(function() { - $rootScope.value = new Date(Date.UTC(2001, 0, 1)); + $rootScope.value = new Date(Date.UTC(2000, 0, 1, 0)); }); - expect(inputElm.val()).toBe('2000-12-31'); + expect(inputElm.val()).toBe('1999-12-31'); // Emptying the input should clear the cached date object helper.changeInputValueTo(''); From 6e618ef00def045b11fa40eaa539f733a8c6ce94 Mon Sep 17 00:00:00 2001 From: Martin Staffa Date: Mon, 4 Dec 2017 22:12:46 +0100 Subject: [PATCH 3/3] fixup! simplify and improve test info --- src/ng/directive/input.js | 9 +++++---- test/ng/directive/inputSpec.js | 32 ++++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js index 4ec6ba26971a..93119d305d42 100644 --- a/src/ng/directive/input.js +++ b/src/ng/directive/input.js @@ -1535,14 +1535,15 @@ function createDateInputType(type, regexp, parseDate, format) { } function parseDateAndConvertTimeZoneToLocal(value, previousDate) { - if (timezone && previousTimezone && previousTimezone !== timezone) { - // If the timezone has changed, adjust the previousDate to the default timzeone + var timezone = ctrl.$options.getOption('timezone'); + + if (previousTimezone && previousTimezone !== timezone) { + // If the timezone has changed, adjust the previousDate to the default timezone // so that the new date is converted with the correct timezone offset - previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone, 0)); + previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone)); } var parsedDate = parseDate(value, previousDate); - var timezone = ctrl.$options.getOption('timezone'); if (!isNaN(parsedDate) && timezone) { parsedDate = convertTimezoneToLocal(parsedDate, timezone); diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js index 104c2df5d2cb..658ce6adf955 100644 --- a/test/ng/directive/inputSpec.js +++ b/test/ng/directive/inputSpec.js @@ -1022,15 +1022,23 @@ describe('input', function() { it('should be possible to override the timezone', function() { var inputElm = helper.compileInput(''); - helper.changeInputValueTo('2013-W03'); - expect(+$rootScope.value).toBe(Date.UTC(2013, 0, 17)); + // January 19 2013 is a Saturday + $rootScope.$apply(function() { + $rootScope.value = new Date(Date.UTC(2013, 0, 19)); + }); + + expect(inputElm.val()).toBe('2013-W03'); - inputElm.controller('ngModel').$overrideModelOptions({timezone: '+5000'}); + inputElm.controller('ngModel').$overrideModelOptions({timezone: '+2400'}); + // To check that the timezone overwrite works, apply an offset of +24 hours. + // Since January 19 is a Saturday, +24 will turn the formatted Date into January 20 - Sunday - + // which is in calendar week 4 instead of 3. $rootScope.$apply(function() { - // the 17. with an offset of +5000 moves the date into next week - $rootScope.value = new Date(Date.UTC(2013, 0, 18)); + $rootScope.value = new Date(Date.UTC(2013, 0, 19)); }); + + // Verifying that the displayed week is week 4 confirms that overriding the timezone worked expect(inputElm.val()).toBe('2013-W04'); }); @@ -2092,30 +2100,34 @@ describe('input', function() { dealoc(formElm); }); - it('should not reuse the hour part of a previous date object after changing the timezone', function() { + it('should not reuse the hours part of a previous date object after changing the timezone', function() { var inputElm = helper.compileInput(''); helper.changeInputValueTo('2000-01-01'); + // The Date parser sets the hours part of the Date to 0 (00:00) (UTC) expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); // Change the timezone offset so that the display date is a day earlier // This does not change the model, but our implementation // internally caches a Date object with this offset - // and re-uses it if part of the date changes + // and re-uses it if part of the Date changes. + // See https://github.com/angular/angular.js/commit/1a1ef62903c8fdf4ceb81277d966a8eff67f0a96 inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'}); $rootScope.$apply(function() { $rootScope.value = new Date(Date.UTC(2000, 0, 1, 0)); }); expect(inputElm.val()).toBe('1999-12-31'); - // Emptying the input should clear the cached date object - helper.changeInputValueTo(''); - + // At this point, the cached Date has its hours set to to 19 (00:00 - 05:00 = 19:00) inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'}); + + // When changing the timezone back to UTC, the hours part of the Date should be set to + // the default 0 (UTC) and not use the modified value of the cached Date object. helper.changeInputValueTo('2000-01-01'); expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0)); }); + describe('min', function() { it('should invalidate', function() {