Skip to content

Commit 3a2aea7

Browse files
authored
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 angular#16181 Closes angular#13382 Closes angular#16336
1 parent b7bb797 commit 3a2aea7

File tree

5 files changed

+142
-2
lines changed

5 files changed

+142
-2
lines changed

src/.eslintrc.json

+1
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"toJsonReplacer": false,
8080
"toJson": false,
8181
"fromJson": false,
82+
"addDateMinutes": false,
8283
"convertTimezoneToLocal": false,
8384
"timezoneToOffset": false,
8485
"startingTag": false,

src/Angular.js

+1
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
fromJson,
7676
convertTimezoneToLocal,
7777
timezoneToOffset,
78+
addDateMinutes,
7879
startingTag,
7980
tryDecodeURIComponent,
8081
parseKeyValue,

src/ng/directive/input.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -1468,11 +1468,12 @@ function createDateInputType(type, regexp, parseDate, format) {
14681468
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
14691469
badInputChecker(scope, element, attr, ctrl, type);
14701470
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
1471-
var timezone = ctrl && ctrl.$options.getOption('timezone');
14721471
var previousDate;
1472+
var previousTimezone;
14731473

14741474
ctrl.$parsers.push(function(value) {
14751475
if (ctrl.$isEmpty(value)) return null;
1476+
14761477
if (regexp.test(value)) {
14771478
// Note: We cannot read ctrl.$modelValue, as there might be a different
14781479
// parser/formatter in the processing chain so that the model
@@ -1489,12 +1490,15 @@ function createDateInputType(type, regexp, parseDate, format) {
14891490
}
14901491
if (isValidDate(value)) {
14911492
previousDate = value;
1492-
if (previousDate && timezone) {
1493+
var timezone = ctrl.$options.getOption('timezone');
1494+
if (timezone) {
1495+
previousTimezone = timezone;
14931496
previousDate = convertTimezoneToLocal(previousDate, timezone, true);
14941497
}
14951498
return $filter('date')(value, format, timezone);
14961499
} else {
14971500
previousDate = null;
1501+
previousTimezone = null;
14981502
return '';
14991503
}
15001504
});
@@ -1531,7 +1535,16 @@ function createDateInputType(type, regexp, parseDate, format) {
15311535
}
15321536

15331537
function parseDateAndConvertTimeZoneToLocal(value, previousDate) {
1538+
var timezone = ctrl.$options.getOption('timezone');
1539+
1540+
if (previousTimezone && previousTimezone !== timezone) {
1541+
// If the timezone has changed, adjust the previousDate to the default timezone
1542+
// so that the new date is converted with the correct timezone offset
1543+
previousDate = addDateMinutes(previousDate, timezoneToOffset(previousTimezone));
1544+
}
1545+
15341546
var parsedDate = parseDate(value, previousDate);
1547+
15351548
if (!isNaN(parsedDate) && timezone) {
15361549
parsedDate = convertTimezoneToLocal(parsedDate, timezone);
15371550
}

src/ng/directive/ngModelOptions.js

+2
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,8 @@ defaultModelOptions = new ModelOptions({
462462
* continental US time zone abbreviations, but for general use, use a time zone offset, for
463463
* example, `'+0430'` (4 hours, 30 minutes east of the Greenwich meridian)
464464
* If not specified, the timezone of the browser will be used.
465+
* Note that changing the timezone will have no effect on the current date, and is only applied after
466+
* the next input / model change.
465467
*
466468
*/
467469
var ngModelOptionsDirective = function() {

test/ng/directive/inputSpec.js

+123
Original file line numberDiff line numberDiff line change
@@ -718,6 +718,21 @@ describe('input', function() {
718718
});
719719

720720

721+
it('should be possible to override the timezone', function() {
722+
var inputElm = helper.compileInput('<input type="month" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
723+
724+
helper.changeInputValueTo('2013-07');
725+
expect(+$rootScope.value).toBe(Date.UTC(2013, 6, 1));
726+
727+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'});
728+
729+
$rootScope.$apply(function() {
730+
$rootScope.value = new Date(Date.UTC(2013, 6, 1));
731+
});
732+
expect(inputElm.val()).toBe('2013-06');
733+
});
734+
735+
721736
they('should use any timezone if specified in the options (format: $prop)',
722737
{'+HHmm': '+0500', '+HH:mm': '+05:00'},
723738
function(tz) {
@@ -1004,6 +1019,30 @@ describe('input', function() {
10041019
});
10051020

10061021

1022+
it('should be possible to override the timezone', function() {
1023+
var inputElm = helper.compileInput('<input type="week" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
1024+
1025+
// January 19 2013 is a Saturday
1026+
$rootScope.$apply(function() {
1027+
$rootScope.value = new Date(Date.UTC(2013, 0, 19));
1028+
});
1029+
1030+
expect(inputElm.val()).toBe('2013-W03');
1031+
1032+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '+2400'});
1033+
1034+
// To check that the timezone overwrite works, apply an offset of +24 hours.
1035+
// Since January 19 is a Saturday, +24 will turn the formatted Date into January 20 - Sunday -
1036+
// which is in calendar week 4 instead of 3.
1037+
$rootScope.$apply(function() {
1038+
$rootScope.value = new Date(Date.UTC(2013, 0, 19));
1039+
});
1040+
1041+
// Verifying that the displayed week is week 4 confirms that overriding the timezone worked
1042+
expect(inputElm.val()).toBe('2013-W04');
1043+
});
1044+
1045+
10071046
they('should use any timezone if specified in the options (format: $prop)',
10081047
{'+HHmm': '+0500', '+HH:mm': '+05:00'},
10091048
function(tz) {
@@ -1229,6 +1268,25 @@ describe('input', function() {
12291268
});
12301269

12311270

1271+
it('should be possible to override the timezone', function() {
1272+
var inputElm = helper.compileInput('<input type="datetime-local" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
1273+
1274+
helper.changeInputValueTo('2000-01-01T01:02');
1275+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0));
1276+
1277+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '+0500'});
1278+
$rootScope.$apply(function() {
1279+
$rootScope.value = new Date(Date.UTC(2001, 0, 1, 1, 2, 0));
1280+
});
1281+
expect(inputElm.val()).toBe('2001-01-01T06:02:00.000');
1282+
1283+
inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'});
1284+
1285+
helper.changeInputValueTo('2000-01-01T01:02');
1286+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 1, 2, 0));
1287+
});
1288+
1289+
12321290
they('should use any timezone if specified in the options (format: $prop)',
12331291
{'+HHmm': '+0500', '+HH:mm': '+05:00'},
12341292
function(tz) {
@@ -1591,6 +1649,25 @@ describe('input', function() {
15911649
});
15921650

15931651

1652+
it('should be possible to override the timezone', function() {
1653+
var inputElm = helper.compileInput('<input type="time" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
1654+
1655+
helper.changeInputValueTo('23:02:00');
1656+
expect(+$rootScope.value).toBe(Date.UTC(1970, 0, 1, 23, 2, 0));
1657+
1658+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'});
1659+
$rootScope.$apply(function() {
1660+
$rootScope.value = new Date(Date.UTC(1971, 0, 1, 23, 2, 0));
1661+
});
1662+
expect(inputElm.val()).toBe('18:02:00.000');
1663+
1664+
inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'});
1665+
helper.changeInputValueTo('23:02:00');
1666+
// The year is still set from the previous date
1667+
expect(+$rootScope.value).toBe(Date.UTC(1971, 0, 1, 23, 2, 0));
1668+
});
1669+
1670+
15941671
they('should use any timezone if specified in the options (format: $prop)',
15951672
{'+HHmm': '+0500', '+HH:mm': '+05:00'},
15961673
function(tz) {
@@ -1920,6 +1997,24 @@ describe('input', function() {
19201997
});
19211998

19221999

2000+
it('should be possible to override the timezone', function() {
2001+
var inputElm = helper.compileInput('<input type="date" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
2002+
2003+
helper.changeInputValueTo('2000-01-01');
2004+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1));
2005+
2006+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'});
2007+
$rootScope.$apply(function() {
2008+
$rootScope.value = new Date(Date.UTC(2001, 0, 1));
2009+
});
2010+
expect(inputElm.val()).toBe('2000-12-31');
2011+
2012+
inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'});
2013+
helper.changeInputValueTo('2000-01-01');
2014+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0));
2015+
});
2016+
2017+
19232018
they('should use any timezone if specified in the options (format: $prop)',
19242019
{'+HHmm': '+0500', '+HH:mm': '+05:00'},
19252020
function(tz) {
@@ -2005,6 +2100,34 @@ describe('input', function() {
20052100
dealoc(formElm);
20062101
});
20072102

2103+
it('should not reuse the hours part of a previous date object after changing the timezone', function() {
2104+
var inputElm = helper.compileInput('<input type="date" ng-model="value" ng-model-options="{timezone: \'UTC\'}" />');
2105+
2106+
helper.changeInputValueTo('2000-01-01');
2107+
// The Date parser sets the hours part of the Date to 0 (00:00) (UTC)
2108+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0));
2109+
2110+
// Change the timezone offset so that the display date is a day earlier
2111+
// This does not change the model, but our implementation
2112+
// internally caches a Date object with this offset
2113+
// and re-uses it if part of the Date changes.
2114+
// See https://github.com/angular/angular.js/commit/1a1ef62903c8fdf4ceb81277d966a8eff67f0a96
2115+
inputElm.controller('ngModel').$overrideModelOptions({timezone: '-0500'});
2116+
$rootScope.$apply(function() {
2117+
$rootScope.value = new Date(Date.UTC(2000, 0, 1, 0));
2118+
});
2119+
expect(inputElm.val()).toBe('1999-12-31');
2120+
2121+
// At this point, the cached Date has its hours set to to 19 (00:00 - 05:00 = 19:00)
2122+
inputElm.controller('ngModel').$overrideModelOptions({timezone: 'UTC'});
2123+
2124+
// When changing the timezone back to UTC, the hours part of the Date should be set to
2125+
// the default 0 (UTC) and not use the modified value of the cached Date object.
2126+
helper.changeInputValueTo('2000-01-01');
2127+
expect(+$rootScope.value).toBe(Date.UTC(2000, 0, 1, 0));
2128+
});
2129+
2130+
20082131
describe('min', function() {
20092132

20102133
it('should invalidate', function() {

0 commit comments

Comments
 (0)