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

Commit 667a78f

Browse files
committed
fix(calendar, datepicker): fix MomentJS custom format support
- revert simplification of `$$mdDateUtil.removeLocalTzAndReparseDate()` - enable unit testing of MomentJS - add test suites for MomentJS custom formatting in `dateLocale.spec.js` and `datepickerDirective.spec.js` Relates to #12003. Relates to #11949.
1 parent a897a67 commit 667a78f

File tree

5 files changed

+149
-56
lines changed

5 files changed

+149
-56
lines changed

config/karma-docs.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ module.exports = function(config) {
1818
'node_modules/angular-messages/angular-messages.js',
1919
'node_modules/angular-route/angular-route.js',
2020
'node_modules/angular-mocks/angular-mocks.js',
21+
'node_modules/moment/moment.js',
2122
'dist/angular-material.js',
2223
'config/test-utils.js',
2324
'dist/docs/docs.js',

config/karma.conf.js

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ module.exports = function(config) {
3838
'node_modules/angular-sanitize/angular-sanitize.js',
3939
'node_modules/angular-touch/angular-touch.js',
4040
'node_modules/angular-mocks/angular-mocks.js',
41+
'node_modules/moment/moment.js',
4142
'test/angular-material-mocks.js',
4243
'test/angular-material-spec.js'
4344
]);

src/components/datepicker/js/dateLocale.spec.js

+37-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
describe('$mdDateLocale', function() {
32
var dateLocale, dateUtil;
43

@@ -81,7 +80,7 @@ describe('$mdDateLocale', function() {
8180

8281
describe('with custom values', function() {
8382
var fakeMonths = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L'];
84-
var fakeshortMonths = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'j', 'l'];
83+
var fakeShortMonths = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'j', 'l'];
8584
var fakeDays = ['D1', 'D2', 'D3', 'D4', 'D5', 'D6', 'D7'];
8685
var fakeShortDays = ['1', '2', '3', '4', '5', '6', '7'];
8786
var fakeDates = [undefined, 'X1', 'X2', 'X3', 'X4', 'X5', 'X6', 'X7', 'X8', 'X9', 'X10', 'X11',
@@ -90,7 +89,7 @@ describe('$mdDateLocale', function() {
9089

9190
beforeEach(module(function($mdDateLocaleProvider) {
9291
$mdDateLocaleProvider.months = fakeMonths;
93-
$mdDateLocaleProvider.shortMonths = fakeshortMonths;
92+
$mdDateLocaleProvider.shortMonths = fakeShortMonths;
9493
$mdDateLocaleProvider.days = fakeDays;
9594
$mdDateLocaleProvider.shortDays = fakeShortDays;
9695
$mdDateLocaleProvider.dates = fakeDates;
@@ -113,7 +112,7 @@ describe('$mdDateLocale', function() {
113112

114113
it('should expose custom settings', function() {
115114
expect(dateLocale.months).toEqual(fakeMonths);
116-
expect(dateLocale.shortMonths).toEqual(fakeshortMonths);
115+
expect(dateLocale.shortMonths).toEqual(fakeShortMonths);
117116
expect(dateLocale.days).toEqual(fakeDays);
118117
expect(dateLocale.shortDays).toEqual(fakeShortDays);
119118
expect(dateLocale.dates).toEqual(fakeDates);
@@ -124,4 +123,38 @@ describe('$mdDateLocale', function() {
124123
expect(dateLocale.isDateComplete('Anything Else')).toBe(false);
125124
});
126125
});
126+
127+
describe('with MomentJS custom formatting', function() {
128+
beforeEach(module(function($mdDateLocaleProvider) {
129+
$mdDateLocaleProvider.formatDate = function(date) {
130+
return date ? moment(date).format('M/D') : '';
131+
};
132+
$mdDateLocaleProvider.parseDate = function(dateString) {
133+
var m = moment(dateString, 'M/D', true);
134+
return m.isValid() ? m.toDate() : new Date(NaN);
135+
};
136+
$mdDateLocaleProvider.isDateComplete = function(dateString) {
137+
dateString = dateString.trim();
138+
// Look for two chunks of content (either numbers or text) separated by delimiters.
139+
var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/;
140+
return re.test(dateString);
141+
};
142+
}));
143+
144+
beforeEach(inject(function($mdDateLocale, $$mdDateUtil) {
145+
dateLocale = $mdDateLocale;
146+
dateUtil = $$mdDateUtil;
147+
}));
148+
149+
it('should respect custom formatting', function() {
150+
var now = new Date();
151+
expect(dateLocale.formatDate(new Date('2020-08-31T00:00:00-04:00'))).toEqual('8/31');
152+
expect(dateLocale.parseDate('8/31')).toEqual(new Date(now.getFullYear(), 7, 31));
153+
expect(dateLocale.parseDate('1/1')).toEqual(new Date(now.getFullYear(), 0, 1));
154+
expect(dateLocale.isDateComplete('8/31')).toBe(true);
155+
expect(dateLocale.isDateComplete('8-31')).toBe(true);
156+
expect(dateLocale.isDateComplete('August_31st')).toBe(false);
157+
expect(dateLocale.isDateComplete('2020')).toBe(false);
158+
});
159+
});
127160
});

src/components/datepicker/js/dateUtil.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,12 @@
314314
* @return {Date} date with local timezone offset removed
315315
*/
316316
function removeLocalTzAndReparseDate(value) {
317-
return $mdDateLocale.parseDate(value.getTime() + 60000 * value.getTimezoneOffset());
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);
318323
}
319324
});
320325
})();

src/components/datepicker/js/datepickerDirective.spec.js

+104-51
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,53 @@
1+
// When constructing a Date, the month is zero-based. This can be confusing, since people are
2+
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
3+
var JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
4+
NOV = 10, DEC = 11;
5+
6+
var initialDate = new Date(2015, FEB, 15);
7+
8+
var ngElement, element, scope, pageScope, controller;
9+
var $compile, $timeout, $$rAF, $animate, $window, keyCodes, dateUtil, dateLocale;
10+
11+
var DATEPICKER_TEMPLATE =
12+
'<md-datepicker name="birthday" ' +
13+
'md-max-date="maxDate" ' +
14+
'md-min-date="minDate" ' +
15+
'md-date-filter="dateFilter" ' +
16+
'md-month-filter="monthFilter" ' +
17+
'ng-model="myDate" ' +
18+
'ng-change="dateChangedHandler()" ' +
19+
'ng-focus="focusHandler()" ' +
20+
'ng-blur="blurHandler()" ' +
21+
'ng-required="isRequired" ' +
22+
'ng-disabled="isDisabled">' +
23+
'</md-datepicker>';
24+
25+
/**
26+
* Compile and link the given template and store values for element, scope, and controller.
27+
* @param {string} template
28+
* @returns {JQLite} The root compiled element.
29+
*/
30+
function createDatepickerInstance(template) {
31+
var outputElement = $compile(template)(pageScope);
32+
pageScope.$apply();
33+
34+
ngElement = outputElement[0].tagName === 'MD-DATEPICKER' ?
35+
outputElement : outputElement.find('md-datepicker');
36+
element = ngElement[0];
37+
scope = ngElement.isolateScope();
38+
controller = ngElement.controller('mdDatepicker');
39+
40+
return outputElement;
41+
}
42+
43+
/** Populates the inputElement with a value and triggers the input events. */
44+
function populateInputElement(inputString) {
45+
controller.ngInputElement.val(inputString).triggerHandler('input');
46+
$timeout.flush();
47+
pageScope.$apply();
48+
}
149

250
describe('md-datepicker', function() {
3-
// When constructing a Date, the month is zero-based. This can be confusing, since people are
4-
// used to seeing them one-based. So we create these aliases to make reading the tests easier.
5-
var JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9,
6-
NOV = 10, DEC = 11;
7-
8-
var initialDate = new Date(2015, FEB, 15);
9-
10-
var ngElement, element, scope, pageScope, controller;
11-
var $compile, $timeout, $$rAF, $animate, $window, keyCodes, dateUtil, dateLocale;
12-
13-
var DATEPICKER_TEMPLATE =
14-
'<md-datepicker name="birthday" ' +
15-
'md-max-date="maxDate" ' +
16-
'md-min-date="minDate" ' +
17-
'md-date-filter="dateFilter"' +
18-
'md-month-filter="monthFilter"' +
19-
'ng-model="myDate" ' +
20-
'ng-change="dateChangedHandler()" ' +
21-
'ng-focus="focusHandler()" ' +
22-
'ng-blur="blurHandler()" ' +
23-
'ng-required="isRequired" ' +
24-
'ng-disabled="isDisabled">' +
25-
'</md-datepicker>';
26-
2751
beforeEach(module('material.components.datepicker', 'material.components.input', 'ngAnimateMock'));
2852

2953
beforeEach(inject(function($rootScope, $injector) {
@@ -51,31 +75,6 @@ describe('md-datepicker', function() {
5175
ngElement.remove();
5276
});
5377

54-
/**
55-
* Compile and link the given template and store values for element, scope, and controller.
56-
* @param {string} template
57-
* @returns {angular.JQLite} The root compiled element.
58-
*/
59-
function createDatepickerInstance(template) {
60-
var outputElement = $compile(template)(pageScope);
61-
pageScope.$apply();
62-
63-
ngElement = outputElement[0].tagName == 'MD-DATEPICKER' ?
64-
outputElement : outputElement.find('md-datepicker');
65-
element = ngElement[0];
66-
scope = ngElement.isolateScope();
67-
controller = ngElement.controller('mdDatepicker');
68-
69-
return outputElement;
70-
}
71-
72-
/** Populates the inputElement with a value and triggers the input events. */
73-
function populateInputElement(inputString) {
74-
controller.ngInputElement.val(inputString).triggerHandler('input');
75-
$timeout.flush();
76-
pageScope.$apply();
77-
}
78-
7978
it('should be the same date object as the initial ng-model', function() {
8079
expect(pageScope.myDate).toBe(initialDate);
8180
});
@@ -591,9 +590,9 @@ describe('md-datepicker', function() {
591590
body.removeChild(element);
592591
});
593592

594-
it('should shink the calendar pane when it would otherwise not fit on the screen', function() {
593+
it('should shrink the calendar pane when it would otherwise not fit on the screen', function() {
595594
// Fake the window being very narrow so that the calendar pane won't fit on-screen.
596-
controller.$window = {innerWidth: 200, innherHeight: 800};
595+
controller.$window = {innerWidth: 200, innerHeight: 800};
597596

598597
// Open the calendar pane.
599598
controller.openCalendarPane({});
@@ -893,3 +892,57 @@ describe('md-datepicker', function() {
893892
});
894893

895894
});
895+
896+
describe('md-datepicker with MomentJS custom formatting', function() {
897+
beforeEach(module('material.components.datepicker', 'material.components.input', 'ngAnimateMock'));
898+
899+
beforeEach(module(function($mdDateLocaleProvider) {
900+
$mdDateLocaleProvider.formatDate = function(date) {
901+
return date ? moment(date).format('M/D') : '';
902+
};
903+
$mdDateLocaleProvider.parseDate = function(dateString) {
904+
var m = moment(dateString, 'M/D', true);
905+
return m.isValid() ? m.toDate() : new Date(NaN);
906+
};
907+
$mdDateLocaleProvider.isDateComplete = function(dateString) {
908+
dateString = dateString.trim();
909+
// Look for two chunks of content (either numbers or text) separated by delimiters.
910+
var re = /^(([a-zA-Z]{3,}|[0-9]{1,4})([ .,]+|[/-]))([a-zA-Z]{3,}|[0-9]{1,4})/;
911+
return re.test(dateString);
912+
};
913+
}));
914+
915+
beforeEach(inject(function($rootScope, $injector) {
916+
$compile = $injector.get('$compile');
917+
$timeout = $injector.get('$timeout');
918+
919+
pageScope = $rootScope.$new();
920+
pageScope.myDate = initialDate;
921+
pageScope.isDisabled = false;
922+
pageScope.dateChangedHandler = jasmine.createSpy('ng-change handler');
923+
924+
createDatepickerInstance(DATEPICKER_TEMPLATE);
925+
controller.closeCalendarPane();
926+
}));
927+
928+
afterEach(function() {
929+
controller.isAttached && controller.closeCalendarPane();
930+
pageScope.$destroy();
931+
ngElement.remove();
932+
});
933+
934+
it('should update the model value and close the calendar pane', function() {
935+
var date = new Date(2020, SEP, 1);
936+
controller.openCalendarPane({
937+
target: controller.inputElement
938+
});
939+
scope.$emit('md-calendar-change', date);
940+
scope.$apply();
941+
expect(pageScope.myDate).toEqual(date);
942+
expect(controller.ngModelCtrl.$modelValue).toEqual(date);
943+
944+
expect(controller.inputElement.value).toEqual('9/1');
945+
expect(controller.calendarPaneOpenedFrom).toBe(null);
946+
expect(controller.isCalendarOpen).toBe(false);
947+
});
948+
});

0 commit comments

Comments
 (0)