Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit e24d09c

Browse files
committedJun 29, 2020
fix(datepicker): support ng-model-options timezone w/ Moment
- fix case where datepicker's model is initially out of sync with the input value - add demo for `ng-model-options` timezone support - pass datepicker's `ng-model-options` on to its calendar Fixes #11945. Fixes #10598.
1 parent 6322e98 commit e24d09c

File tree

9 files changed

+88
-23
lines changed

9 files changed

+88
-23
lines changed
 

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -132,4 +132,4 @@
132132
"node": ">=10",
133133
"npm": ">=6"
134134
}
135-
}
135+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<md-content ng-controller="AppCtrl as ctrl" layout="row" layout-padding ng-cloak>
2+
<md-calendar ng-model="ctrl.calendarDate" ng-model-options="{timezone: 'UTC'}">
3+
</md-calendar>
4+
5+
<div layout="column" layout-padding>
6+
<div>
7+
<h4>Calendar Values</h4>
8+
<div>
9+
<strong>Date in local timezone:</strong>
10+
{{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z"}}
11+
</div>
12+
<div>
13+
<strong>Date in UTC timezone:</strong>
14+
{{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}}
15+
</div>
16+
</div>
17+
<md-divider></md-divider>
18+
<md-datepicker ng-model="ctrl.datepickerDate" ng-model-options="{timezone: 'UTC'}">
19+
</md-datepicker>
20+
<div>
21+
<h4>Datepicker Values</h4>
22+
<div>
23+
<strong>Date in local timezone:</strong>
24+
{{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z"}}
25+
</div>
26+
<div>
27+
<strong>Date in UTC timezone:</strong>
28+
{{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}}
29+
</div>
30+
</div>
31+
</div>
32+
</md-content>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
angular.module('ngModelTimezoneUsage', ['ngMaterial', 'ngMessages'])
2+
.controller('AppCtrl', function() {
3+
this.datepickerDate = new Date(0);
4+
this.datepickerDate.setUTCFullYear(2020, 5, 19);
5+
6+
this.calendarDate = new Date(0);
7+
this.calendarDate.setUTCFullYear(2020, 5, 19);
8+
});

‎src/components/datepicker/js/calendar.js

+4-7
Original file line numberDiff line numberDiff line change
@@ -321,17 +321,14 @@
321321
}, this.$attrs, [ngModelCtrl]);
322322

323323
ngModelCtrl.$render = function() {
324-
var value = this.$viewValue;
325-
var parsedValue, convertedValue;
324+
var value = this.$viewValue, convertedDate;
326325

327326
// In the case where a conversion is needed, the $viewValue here will be a string like
328327
// "2020-05-10" instead of a Date object.
329328
if (!self.dateUtil.isValidDate(value)) {
330-
parsedValue = self.$mdDateLocale.parseDate(this.$viewValue);
331-
convertedValue =
332-
new Date(parsedValue.getTime() + 60000 * parsedValue.getTimezoneOffset());
333-
if (self.dateUtil.isValidDate(convertedValue)) {
334-
value = convertedValue;
329+
convertedDate = self.dateUtil.removeLocalTzAndReparseDate(new Date(this.$viewValue));
330+
if (self.dateUtil.isValidDate(convertedDate)) {
331+
value = convertedDate;
335332
}
336333
}
337334

‎src/components/datepicker/js/calendarMonth.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@
9898
this.cellClickHandler = function() {
9999
var timestamp = $$mdDateUtil.getTimestampFromNode(this);
100100
self.$scope.$apply(function() {
101-
self.calendarCtrl.setNgModelValue(self.dateLocale.parseDate(timestamp));
101+
// The timestamp has to be converted to a valid date.
102+
self.calendarCtrl.setNgModelValue(new Date(timestamp));
102103
});
103104
};
104105

‎src/components/datepicker/js/calendarYear.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,8 @@
227227

228228
if (calendarCtrl.mode) {
229229
this.$mdUtil.nextTick(function() {
230-
calendarCtrl.setNgModelValue(calendarCtrl.$mdDateLocale.parseDate(timestamp));
230+
// The timestamp has to be converted to a valid date.
231+
calendarCtrl.setNgModelValue(new Date(timestamp));
231232
});
232233
} else {
233234
calendarCtrl.setCurrentView('month', timestamp);

‎src/components/datepicker/js/dateUtil.js

+16-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Utility for performing date calculations to facilitate operation of the calendar and
66
* datepicker.
77
*/
8-
angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
8+
angular.module('material.components.datepicker').factory('$$mdDateUtil', function($mdDateLocale) {
99
return {
1010
getFirstDateOfMonth: getFirstDateOfMonth,
1111
getNumberOfDaysInMonth: getNumberOfDaysInMonth,
@@ -29,7 +29,8 @@
2929
getYearDistance: getYearDistance,
3030
clampDate: clampDate,
3131
getTimestampFromNode: getTimestampFromNode,
32-
isMonthWithinRange: isMonthWithinRange
32+
isMonthWithinRange: isMonthWithinRange,
33+
removeLocalTzAndReparseDate: removeLocalTzAndReparseDate
3334
};
3435

3536
/**
@@ -307,5 +308,18 @@
307308
return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
308309
(!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
309310
}
311+
312+
/**
313+
* @param {Date} value
314+
* @return {boolean|boolean}
315+
*/
316+
function removeLocalTzAndReparseDate(value) {
317+
var dateValue, formattedDate;
318+
// Remove the local timezone offset before calling formatDate.
319+
dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset());
320+
formattedDate = $mdDateLocale.formatDate(dateValue);
321+
// parseDate only works with a date formatted by formatDate when using Moment validation.
322+
return $mdDateLocale.parseDate(formattedDate);
323+
}
310324
});
311325
})();

‎src/components/datepicker/js/datepickerDirective.js

+20-8
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
// may be confusing.
8686
var hiddenIcons = tAttrs.mdHideIcons;
8787
var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
88+
var ngModelOptions = tAttrs.ngModelOptions;
8889

8990
var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
9091
'<md-button class="md-datepicker-button md-icon-button" type="button" ' +
@@ -132,6 +133,7 @@
132133
'md-max-date="ctrl.maxDate" ' +
133134
'md-date-filter="ctrl.dateFilter" ' +
134135
'md-month-filter="ctrl.monthFilter" ' +
136+
(ngModelOptions ? 'ng-model-options="' + ngModelOptions + '" ' : '') +
135137
'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
136138
'</md-calendar>' +
137139
'</div>' +
@@ -180,7 +182,8 @@
180182
mdInputContainer.input = element;
181183
mdInputContainer.element
182184
.addClass(INPUT_CONTAINER_CLASS)
183-
.toggleClass(HAS_CALENDAR_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
185+
.toggleClass(HAS_CALENDAR_ICON_CLASS,
186+
attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
184187

185188
if (!mdInputContainer.label) {
186189
$mdAria.expect(element, 'aria-label', attr.mdPlaceholder);
@@ -191,7 +194,8 @@
191194
}
192195

193196
scope.$watch(mdInputContainer.isErrorGetter || function() {
194-
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
197+
return ngModelCtrl.$invalid && (ngModelCtrl.$touched ||
198+
(parentForm && parentForm.$submitted));
195199
}, mdInputContainer.setInvalid);
196200
} else if (parentForm) {
197201
// If invalid, highlights the input when the parent form is submitted.
@@ -424,16 +428,17 @@
424428
});
425429
}
426430

427-
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are pre-assigned,
428-
// manually call the $onInit hook.
431+
// For AngularJS 1.4 and older, where there are no lifecycle hooks but bindings are
432+
// pre-assigned, manually call the $onInit hook.
429433
if (angular.version.major === 1 && angular.version.minor <= 4) {
430434
this.$onInit();
431435
}
432436
}
433437

434438
/**
435439
* AngularJS Lifecycle hook for newer AngularJS versions.
436-
* Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
440+
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
441+
* $onInit hook.
437442
*/
438443
DatePickerCtrl.prototype.$onInit = function() {
439444

@@ -442,7 +447,8 @@
442447
* the user to override specific ones from the $mdDateLocale provider.
443448
* @type {!Object}
444449
*/
445-
this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale) : this.$mdDateLocale;
450+
this.locale = this.dateLocale ? angular.extend({}, this.$mdDateLocale, this.dateLocale)
451+
: this.$mdDateLocale;
446452

447453
this.installPropertyInterceptors();
448454
this.attachChangeListeners();
@@ -743,7 +749,9 @@
743749
var bodyRect = body.getBoundingClientRect();
744750

745751
if (!this.topMargin || this.topMargin < 0) {
746-
this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
752+
this.topMargin =
753+
(this.inputMask.parent().prop('clientHeight')
754+
- this.ngInputElement.prop('clientHeight')) / 2;
747755
}
748756

749757
// Check to see if the calendar pane would go off the screen. If so, adjust position
@@ -993,7 +1001,11 @@
9931001
var self = this;
9941002
var timezone = this.$mdUtil.getModelOption(this.ngModelCtrl, 'timezone');
9951003

996-
this.date = value;
1004+
if (this.dateUtil.isValidDate(value)) {
1005+
this.date = this.dateUtil.removeLocalTzAndReparseDate(value);
1006+
} else {
1007+
this.date = value;
1008+
}
9971009
this.inputElement.value = this.locale.formatDate(value, timezone);
9981010
this.mdInputContainer && this.mdInputContainer.setHasValue(!!value);
9991011
this.resizeInputElement();

‎src/core/util/util.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
8484
* which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
8585
* @param {!ngModel.NgModelController} ngModelCtrl
8686
* @param {!string} optionName
87-
* @returns {Object|undefined}
87+
* @returns {string|number|boolean|Object|undefined}
8888
*/
8989
getModelOption: function (ngModelCtrl, optionName) {
9090
if (!ngModelCtrl.$options) {
@@ -93,8 +93,8 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
9393

9494
var $options = ngModelCtrl.$options;
9595

96-
// The newer versions of AngularJS introduced a `getOption function and made the option values no longer
97-
// visible on the $options object.
96+
// The newer versions of AngularJS introduced a getOption function and made the option values
97+
// no longer visible on the $options object.
9898
return $options.getOption ? $options.getOption(optionName) : $options[optionName];
9999
},
100100

0 commit comments

Comments
 (0)
Failed to load comments.