Skip to content
This repository was archived by the owner on Sep 5, 2024. It is now read-only.

Commit

Permalink
fix(datepicker): support ng-model-options timezone w/ Moment
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
Splaktar committed Jun 29, 2020
1 parent 6322e98 commit e24d09c
Show file tree
Hide file tree
Showing 9 changed files with 88 additions and 23 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,4 @@
"node": ">=10",
"npm": ">=6"
}
}
}
32 changes: 32 additions & 0 deletions src/components/datepicker/demoNgModelOptionsTimezone/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<md-content ng-controller="AppCtrl as ctrl" layout="row" layout-padding ng-cloak>
<md-calendar ng-model="ctrl.calendarDate" ng-model-options="{timezone: 'UTC'}">
</md-calendar>

<div layout="column" layout-padding>
<div>
<h4>Calendar Values</h4>
<div>
<strong>Date in local timezone:</strong>
{{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z"}}
</div>
<div>
<strong>Date in UTC timezone:</strong>
{{ctrl.calendarDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}}
</div>
</div>
<md-divider></md-divider>
<md-datepicker ng-model="ctrl.datepickerDate" ng-model-options="{timezone: 'UTC'}">
</md-datepicker>
<div>
<h4>Datepicker Values</h4>
<div>
<strong>Date in local timezone:</strong>
{{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z"}}
</div>
<div>
<strong>Date in UTC timezone:</strong>
{{ctrl.datepickerDate|date:"yyyy-MM-dd HH:mm Z":"UTC"}}
</div>
</div>
</div>
</md-content>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
angular.module('ngModelTimezoneUsage', ['ngMaterial', 'ngMessages'])
.controller('AppCtrl', function() {
this.datepickerDate = new Date(0);
this.datepickerDate.setUTCFullYear(2020, 5, 19);

this.calendarDate = new Date(0);
this.calendarDate.setUTCFullYear(2020, 5, 19);
});
11 changes: 4 additions & 7 deletions src/components/datepicker/js/calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,17 +321,14 @@
}, this.$attrs, [ngModelCtrl]);

ngModelCtrl.$render = function() {
var value = this.$viewValue;
var parsedValue, convertedValue;
var value = this.$viewValue, convertedDate;

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

Expand Down
3 changes: 2 additions & 1 deletion src/components/datepicker/js/calendarMonth.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@
this.cellClickHandler = function() {
var timestamp = $$mdDateUtil.getTimestampFromNode(this);
self.$scope.$apply(function() {
self.calendarCtrl.setNgModelValue(self.dateLocale.parseDate(timestamp));
// The timestamp has to be converted to a valid date.
self.calendarCtrl.setNgModelValue(new Date(timestamp));
});
};

Expand Down
3 changes: 2 additions & 1 deletion src/components/datepicker/js/calendarYear.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,8 @@

if (calendarCtrl.mode) {
this.$mdUtil.nextTick(function() {
calendarCtrl.setNgModelValue(calendarCtrl.$mdDateLocale.parseDate(timestamp));
// The timestamp has to be converted to a valid date.
calendarCtrl.setNgModelValue(new Date(timestamp));
});
} else {
calendarCtrl.setCurrentView('month', timestamp);
Expand Down
18 changes: 16 additions & 2 deletions src/components/datepicker/js/dateUtil.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* Utility for performing date calculations to facilitate operation of the calendar and
* datepicker.
*/
angular.module('material.components.datepicker').factory('$$mdDateUtil', function() {
angular.module('material.components.datepicker').factory('$$mdDateUtil', function($mdDateLocale) {
return {
getFirstDateOfMonth: getFirstDateOfMonth,
getNumberOfDaysInMonth: getNumberOfDaysInMonth,
Expand All @@ -29,7 +29,8 @@
getYearDistance: getYearDistance,
clampDate: clampDate,
getTimestampFromNode: getTimestampFromNode,
isMonthWithinRange: isMonthWithinRange
isMonthWithinRange: isMonthWithinRange,
removeLocalTzAndReparseDate: removeLocalTzAndReparseDate
};

/**
Expand Down Expand Up @@ -307,5 +308,18 @@
return (!minDate || minDate.getFullYear() < year || minDate.getMonth() <= month) &&
(!maxDate || maxDate.getFullYear() > year || maxDate.getMonth() >= month);
}

/**
* @param {Date} value
* @return {boolean|boolean}
*/
function removeLocalTzAndReparseDate(value) {
var dateValue, formattedDate;
// Remove the local timezone offset before calling formatDate.
dateValue = new Date(value.getTime() + 60000 * value.getTimezoneOffset());
formattedDate = $mdDateLocale.formatDate(dateValue);
// parseDate only works with a date formatted by formatDate when using Moment validation.
return $mdDateLocale.parseDate(formattedDate);
}
});
})();
28 changes: 20 additions & 8 deletions src/components/datepicker/js/datepickerDirective.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
// may be confusing.
var hiddenIcons = tAttrs.mdHideIcons;
var ariaLabelValue = tAttrs.ariaLabel || tAttrs.mdPlaceholder;
var ngModelOptions = tAttrs.ngModelOptions;

var calendarButton = (hiddenIcons === 'all' || hiddenIcons === 'calendar') ? '' :
'<md-button class="md-datepicker-button md-icon-button" type="button" ' +
Expand Down Expand Up @@ -132,6 +133,7 @@
'md-max-date="ctrl.maxDate" ' +
'md-date-filter="ctrl.dateFilter" ' +
'md-month-filter="ctrl.monthFilter" ' +
(ngModelOptions ? 'ng-model-options="' + ngModelOptions + '" ' : '') +
'ng-model="ctrl.date" ng-if="ctrl.isCalendarOpen">' +
'</md-calendar>' +
'</div>' +
Expand Down Expand Up @@ -180,7 +182,8 @@
mdInputContainer.input = element;
mdInputContainer.element
.addClass(INPUT_CONTAINER_CLASS)
.toggleClass(HAS_CALENDAR_ICON_CLASS, attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');
.toggleClass(HAS_CALENDAR_ICON_CLASS,
attr.mdHideIcons !== 'calendar' && attr.mdHideIcons !== 'all');

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

scope.$watch(mdInputContainer.isErrorGetter || function() {
return ngModelCtrl.$invalid && (ngModelCtrl.$touched || (parentForm && parentForm.$submitted));
return ngModelCtrl.$invalid && (ngModelCtrl.$touched ||
(parentForm && parentForm.$submitted));
}, mdInputContainer.setInvalid);
} else if (parentForm) {
// If invalid, highlights the input when the parent form is submitted.
Expand Down Expand Up @@ -424,16 +428,17 @@
});
}

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

/**
* AngularJS Lifecycle hook for newer AngularJS versions.
* Bindings are not guaranteed to have been assigned in the controller, but they are in the $onInit hook.
* Bindings are not guaranteed to have been assigned in the controller, but they are in the
* $onInit hook.
*/
DatePickerCtrl.prototype.$onInit = function() {

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

this.installPropertyInterceptors();
this.attachChangeListeners();
Expand Down Expand Up @@ -743,7 +749,9 @@
var bodyRect = body.getBoundingClientRect();

if (!this.topMargin || this.topMargin < 0) {
this.topMargin = (this.inputMask.parent().prop('clientHeight') - this.ngInputElement.prop('clientHeight')) / 2;
this.topMargin =
(this.inputMask.parent().prop('clientHeight')
- this.ngInputElement.prop('clientHeight')) / 2;
}

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

this.date = value;
if (this.dateUtil.isValidDate(value)) {
this.date = this.dateUtil.removeLocalTzAndReparseDate(value);
} else {
this.date = value;
}
this.inputElement.value = this.locale.formatDate(value, timezone);
this.mdInputContainer && this.mdInputContainer.setHasValue(!!value);
this.resizeInputElement();
Expand Down
6 changes: 3 additions & 3 deletions src/core/util/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in
* which supports the breaking changes in the AngularJS snapshot (SHA 87a2ff76af5d0a9268d8eb84db5755077d27c84c).
* @param {!ngModel.NgModelController} ngModelCtrl
* @param {!string} optionName
* @returns {Object|undefined}
* @returns {string|number|boolean|Object|undefined}
*/
getModelOption: function (ngModelCtrl, optionName) {
if (!ngModelCtrl.$options) {
Expand All @@ -93,8 +93,8 @@ function UtilFactory($document, $timeout, $compile, $rootScope, $$mdAnimate, $in

var $options = ngModelCtrl.$options;

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

Expand Down

0 comments on commit e24d09c

Please sign in to comment.