mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(datepicker): Add Moment.js adapter (#6860)
* create moment adapter * add Moment to karma.conf.js * use MAT_DATE_LOCALE * tests are now running * add in tests * WIP: demo * add synthetic default imports for moment * add locale switching to demo * fix moment import issues with tests * fix aot * address comments * fix closure issue * add additional explanation of rollup issue * remove moment adapter demo since it is preventing sync into g3 (will add back as a docs example in future PR). * fix bad rebase
This commit is contained in:
@@ -101,6 +101,7 @@
|
||||
"magic-string": "^0.21.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimist": "^1.2.0",
|
||||
"moment": "^2.18.1",
|
||||
"node-sass": "^4.5.3",
|
||||
"protractor": "^5.1.2",
|
||||
"request": "^2.81.0",
|
||||
|
||||
@@ -8,9 +8,10 @@ set -e -o pipefail
|
||||
# Go to the project root directory
|
||||
cd $(dirname $0)/../..
|
||||
|
||||
# Build a release of material and of the CDK package.
|
||||
# Build a release of material, material-moment-adapter, and cdk packages.
|
||||
$(npm bin)/gulp material:build-release:clean
|
||||
$(npm bin)/gulp cdk:build-release
|
||||
$(npm bin)/gulp material-moment-adapter:build-release
|
||||
|
||||
# Build demo-app with ES2015 modules. Closure compiler is then able to parse imports.
|
||||
$(npm bin)/gulp :build:devapp:assets :build:devapp:scss
|
||||
@@ -38,6 +39,7 @@ OPTS=(
|
||||
"--js_module_root=dist/packages"
|
||||
"--js_module_root=dist/releases/material"
|
||||
"--js_module_root=dist/releases/cdk"
|
||||
"--js_module_root=dist/releases/material-moment-adapter"
|
||||
"--js_module_root=node_modules/@angular/core"
|
||||
"--js_module_root=node_modules/@angular/common"
|
||||
"--js_module_root=node_modules/@angular/compiler"
|
||||
@@ -49,6 +51,7 @@ OPTS=(
|
||||
"--js_module_root=node_modules/@angular/platform-browser-dynamic"
|
||||
"--js_module_root=node_modules/@angular/animations"
|
||||
"--js_module_root=node_modules/@angular/animations/browser"
|
||||
"--js_module_root=node_modules/moment"
|
||||
|
||||
# Flags to simplify debugging.
|
||||
"--formatting=PRETTY_PRINT"
|
||||
@@ -57,6 +60,7 @@ OPTS=(
|
||||
# Include the Material and CDK FESM bundles
|
||||
dist/releases/material/@angular/material.js
|
||||
dist/releases/cdk/@angular/cdk.js
|
||||
dist/releases/material-moment-adapter/@angular/material-moment-adapter.js
|
||||
|
||||
# Include all Angular FESM bundles.
|
||||
node_modules/@angular/core/@angular/core.js
|
||||
@@ -71,8 +75,9 @@ OPTS=(
|
||||
node_modules/@angular/animations/@angular/animations.js
|
||||
node_modules/@angular/animations/@angular/animations/browser.js
|
||||
|
||||
# Include other dependencies like Zone.js and RxJS
|
||||
# Include other dependencies like Zone.js, Moment.js, and RxJS
|
||||
node_modules/zone.js/dist/zone.js
|
||||
node_modules/moment/moment.js
|
||||
$rxjsSourceFiles
|
||||
|
||||
# Include all files from the demo-app package.
|
||||
|
||||
@@ -9,6 +9,7 @@ System.config({
|
||||
map: {
|
||||
'rxjs': 'node:rxjs',
|
||||
'main': 'main.js',
|
||||
'moment': 'node:moment/min/moment-with-locales.min.js',
|
||||
|
||||
// Angular specific mappings.
|
||||
'@angular/core': 'node:@angular/core/bundles/core.umd.js',
|
||||
@@ -26,6 +27,7 @@ System.config({
|
||||
'node:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
|
||||
|
||||
'@angular/material': 'dist/bundles/material.umd.js',
|
||||
'@angular/material-moment-adapter': 'dist/bundles/material-moment-adapter.umd.js',
|
||||
'@angular/cdk': 'dist/bundles/cdk.umd.js',
|
||||
'@angular/cdk/a11y': 'dist/bundles/cdk-a11y.umd.js',
|
||||
'@angular/cdk/bidi': 'dist/bundles/cdk-bidi.umd.js',
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"outDir": ".",
|
||||
"paths": {
|
||||
"@angular/material": ["./material"],
|
||||
"@angular/cdk/*": ["./cdk/*"]
|
||||
"@angular/cdk/*": ["./cdk/*"],
|
||||
"@angular/material-moment-adapter": ["./material-moment-adapter"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -22,7 +22,8 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@angular/material": ["../../dist/packages/material/public_api"],
|
||||
"@angular/cdk/*": ["../../dist/packages/cdk/*"]
|
||||
"@angular/cdk/*": ["../../dist/packages/cdk/*"],
|
||||
"@angular/material-moment-adapter": ["../../dist/packages/material-moment-adapter"]
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
*/
|
||||
|
||||
import {InjectionToken, LOCALE_ID} from '@angular/core';
|
||||
import {Observable} from 'rxjs/Observable';
|
||||
import {Subject} from 'rxjs/Subject';
|
||||
|
||||
|
||||
/** InjectionToken for datepicker that can be used to override default locale code. */
|
||||
export const MAT_DATE_LOCALE = new InjectionToken<string>('MAT_DATE_LOCALE');
|
||||
@@ -19,6 +22,10 @@ export abstract class DateAdapter<D> {
|
||||
/** The locale to use for all dates. */
|
||||
protected locale: any;
|
||||
|
||||
/** A stream that emits when the locale changes. */
|
||||
get localeChanges(): Observable<void> { return this._localeChanges; }
|
||||
protected _localeChanges= new Subject<void>();
|
||||
|
||||
/**
|
||||
* Gets the year component of the given date.
|
||||
* @param date The date to extract the year from.
|
||||
@@ -184,6 +191,7 @@ export abstract class DateAdapter<D> {
|
||||
*/
|
||||
setLocale(locale: any) {
|
||||
this.locale = locale;
|
||||
this._localeChanges.next();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -182,6 +182,8 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
|
||||
|
||||
private _datepickerSubscription = Subscription.EMPTY;
|
||||
|
||||
private _localeSubscription = Subscription.EMPTY;
|
||||
|
||||
/** The form control validator for whether the input parses. */
|
||||
private _parseValidator: ValidatorFn = (): ValidationErrors | null => {
|
||||
return this._lastValueValid ?
|
||||
@@ -228,6 +230,11 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
|
||||
if (!this._dateFormats) {
|
||||
throw createMissingDateImplError('MD_DATE_FORMATS');
|
||||
}
|
||||
|
||||
// Update the displayed date when the locale changes.
|
||||
this._localeSubscription = _dateAdapter.localeChanges.subscribe(() => {
|
||||
this.value = this.value;
|
||||
});
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
@@ -245,6 +252,7 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
|
||||
|
||||
ngOnDestroy() {
|
||||
this._datepickerSubscription.unsubscribe();
|
||||
this._localeSubscription.unsubscribe();
|
||||
this._valueChange.complete();
|
||||
this._disabledChange.complete();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {NgModule} from '@angular/core';
|
||||
import {
|
||||
DateAdapter,
|
||||
MAT_DATE_LOCALE,
|
||||
MAT_DATE_LOCALE_PROVIDER,
|
||||
MD_DATE_FORMATS
|
||||
} from '@angular/material';
|
||||
import {MomentDateAdapter} from './moment-date-adapter';
|
||||
import {MD_MOMENT_DATE_FORMATS} from './moment-date-formats';
|
||||
|
||||
export * from './moment-date-adapter';
|
||||
export * from './moment-date-formats';
|
||||
|
||||
|
||||
@NgModule({
|
||||
providers: [
|
||||
MAT_DATE_LOCALE_PROVIDER,
|
||||
{provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]}
|
||||
],
|
||||
})
|
||||
export class MomentDateModule {}
|
||||
|
||||
|
||||
@NgModule({
|
||||
imports: [MomentDateModule],
|
||||
providers: [{provide: MD_DATE_FORMATS, useValue: MD_MOMENT_DATE_FORMATS}],
|
||||
})
|
||||
export class MdMomentDateModule {}
|
||||
@@ -0,0 +1,388 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MomentDateAdapter} from './moment-date-adapter';
|
||||
import {async, inject, TestBed} from '@angular/core/testing';
|
||||
import {MomentDateModule} from './index';
|
||||
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
|
||||
import {LOCALE_ID} from '@angular/core';
|
||||
import * as moment from 'moment';
|
||||
|
||||
|
||||
// Month constants for more readable tests.
|
||||
const JAN = 0, FEB = 1, MAR = 2, DEC = 11;
|
||||
|
||||
|
||||
describe('MomentDateAdapter', () => {
|
||||
let adapter: MomentDateAdapter;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MomentDateModule]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([DateAdapter], (d: MomentDateAdapter) => {
|
||||
moment.locale('en');
|
||||
adapter = d;
|
||||
adapter.setLocale('en');
|
||||
}));
|
||||
|
||||
it('should get year', () => {
|
||||
expect(adapter.getYear(moment([2017, JAN, 1]))).toBe(2017);
|
||||
});
|
||||
|
||||
it('should get month', () => {
|
||||
expect(adapter.getMonth(moment([2017, JAN, 1]))).toBe(0);
|
||||
});
|
||||
|
||||
it('should get date', () => {
|
||||
expect(adapter.getDate(moment([2017, JAN, 1]))).toBe(1);
|
||||
});
|
||||
|
||||
it('should get day of week', () => {
|
||||
expect(adapter.getDayOfWeek(moment([2017, JAN, 1]))).toBe(0);
|
||||
});
|
||||
|
||||
it('should get same day of week in a locale with a different first day of the week', () => {
|
||||
adapter.setLocale('fr');
|
||||
expect(adapter.getDayOfWeek(moment([2017, JAN, 1]))).toBe(0);
|
||||
});
|
||||
|
||||
it('should get long month names', () => {
|
||||
expect(adapter.getMonthNames('long')).toEqual([
|
||||
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
|
||||
'October', 'November', 'December'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get long month names', () => {
|
||||
expect(adapter.getMonthNames('short')).toEqual([
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get narrow month names', () => {
|
||||
expect(adapter.getMonthNames('narrow')).toEqual([
|
||||
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get month names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.getMonthNames('long')).toEqual([
|
||||
'1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get date names', () => {
|
||||
expect(adapter.getDateNames()).toEqual([
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
|
||||
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get date names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.getDateNames()).toEqual([
|
||||
'1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17',
|
||||
'18', '19', '20', '21', '22', '23', '24', '25', '26', '27', '28', '29', '30', '31'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get long day of week names', () => {
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual([
|
||||
'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get short day of week names', () => {
|
||||
expect(adapter.getDayOfWeekNames('short')).toEqual([
|
||||
'Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get narrow day of week names', () => {
|
||||
expect(adapter.getDayOfWeekNames('narrow')).toEqual([
|
||||
'Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get day of week names in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.getDayOfWeekNames('long')).toEqual([
|
||||
'日曜日', '月曜日', '火曜日', '水曜日', '木曜日', '金曜日', '土曜日'
|
||||
]);
|
||||
});
|
||||
|
||||
it('should get year name', () => {
|
||||
expect(adapter.getYearName(moment([2017, JAN, 1]))).toBe('2017');
|
||||
});
|
||||
|
||||
it('should get year name in a different locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.getYearName(moment([2017, JAN, 1]))).toBe('2017');
|
||||
});
|
||||
|
||||
it('should get first day of week', () => {
|
||||
expect(adapter.getFirstDayOfWeek()).toBe(0);
|
||||
});
|
||||
|
||||
it('should get first day of week in a different locale', () => {
|
||||
adapter.setLocale('fr');
|
||||
expect(adapter.getFirstDayOfWeek()).toBe(1);
|
||||
});
|
||||
|
||||
it('should create Moment date', () => {
|
||||
expect(adapter.createDate(2017, JAN, 1).format()).toEqual(moment([2017, JAN, 1]).format());
|
||||
});
|
||||
|
||||
it('should not create Moment date with month over/under-flow', () => {
|
||||
expect(() => adapter.createDate(2017, DEC + 1, 1)).toThrow();
|
||||
expect(() => adapter.createDate(2017, JAN - 1, 1)).toThrow();
|
||||
});
|
||||
|
||||
it('should not create Moment date with date over/under-flow', () => {
|
||||
expect(() => adapter.createDate(2017, JAN, 32)).toThrow();
|
||||
expect(() => adapter.createDate(2017, JAN, 0)).toThrow();
|
||||
});
|
||||
|
||||
it('should create Moment date with low year number', () => {
|
||||
expect(adapter.createDate(-1, JAN, 1).year()).toBe(-1);
|
||||
expect(adapter.createDate(0, JAN, 1).year()).toBe(0);
|
||||
expect(adapter.createDate(50, JAN, 1).year()).toBe(50);
|
||||
expect(adapter.createDate(99, JAN, 1).year()).toBe(99);
|
||||
expect(adapter.createDate(100, JAN, 1).year()).toBe(100);
|
||||
});
|
||||
|
||||
it("should get today's date", () => {
|
||||
expect(adapter.sameDate(adapter.today(), moment()))
|
||||
.toBe(true, "should be equal to today's date");
|
||||
});
|
||||
|
||||
it('should parse string according to given format', () => {
|
||||
expect(adapter.parse('1/2/2017', 'MM/DD/YYYY')!.format())
|
||||
.toEqual(moment([2017, JAN, 2]).format());
|
||||
expect(adapter.parse('1/2/2017', 'DD/MM/YYYY')!.format())
|
||||
.toEqual(moment([2017, FEB, 1]).format());
|
||||
});
|
||||
|
||||
it('should parse number', () => {
|
||||
let timestamp = new Date().getTime();
|
||||
expect(adapter.parse(timestamp, 'MM/DD/YYYY')!.format()).toEqual(moment(timestamp).format());
|
||||
});
|
||||
|
||||
it ('should parse Date', () => {
|
||||
let date = new Date(2017, JAN, 1);
|
||||
expect(adapter.parse(date, 'MM/DD/YYYY')!.format()).toEqual(moment(date).format());
|
||||
});
|
||||
|
||||
it ('should parse Moment date', () => {
|
||||
let date = moment([2017, JAN, 1]);
|
||||
let parsedDate = adapter.parse(date, 'MM/DD/YYYY');
|
||||
expect(parsedDate!.format()).toEqual(date.format());
|
||||
expect(parsedDate).not.toBe(date);
|
||||
});
|
||||
|
||||
it('should parse empty string as null', () => {
|
||||
expect(adapter.parse('', 'MM/DD/YYYY')).toBeNull();
|
||||
});
|
||||
|
||||
it('should parse invalid value as invalid', () => {
|
||||
let d = adapter.parse('hello', 'MM/DD/YYYY');
|
||||
expect(d).not.toBeNull();
|
||||
expect(adapter.isDateInstance(d))
|
||||
.toBe(true, 'Expected string to have been fed through Date.parse');
|
||||
expect(adapter.isValid(d as moment.Moment))
|
||||
.toBe(false, 'Expected to parse as "invalid date" object');
|
||||
});
|
||||
|
||||
it('should format date according to given format', () => {
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'MM/DD/YYYY')).toEqual('01/02/2017');
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'DD/MM/YYYY')).toEqual('02/01/2017');
|
||||
});
|
||||
|
||||
it('should format with a different locale', () => {
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'll')).toEqual('Jan 2, 2017');
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'll')).toEqual('2017年1月2日');
|
||||
});
|
||||
|
||||
it('should throw when attempting to format invalid date', () => {
|
||||
expect(() => adapter.format(moment(NaN), 'MM/DD/YYYY'))
|
||||
.toThrowError(/MomentDateAdapter: Cannot format invalid date\./);
|
||||
});
|
||||
|
||||
it('should add years', () => {
|
||||
expect(adapter.addCalendarYears(moment([2017, JAN, 1]), 1).format())
|
||||
.toEqual(moment([2018, JAN, 1]).format());
|
||||
expect(adapter.addCalendarYears(moment([2017, JAN, 1]), -1).format())
|
||||
.toEqual(moment([2016, JAN, 1]).format());
|
||||
});
|
||||
|
||||
it('should respect leap years when adding years', () => {
|
||||
expect(adapter.addCalendarYears(moment([2016, FEB, 29]), 1).format())
|
||||
.toEqual(moment([2017, FEB, 28]).format());
|
||||
expect(adapter.addCalendarYears(moment([2016, FEB, 29]), -1).format())
|
||||
.toEqual(moment([2015, FEB, 28]).format());
|
||||
});
|
||||
|
||||
it('should add months', () => {
|
||||
expect(adapter.addCalendarMonths(moment([2017, JAN, 1]), 1).format())
|
||||
.toEqual(moment([2017, FEB, 1]).format());
|
||||
expect(adapter.addCalendarMonths(moment([2017, JAN, 1]), -1).format())
|
||||
.toEqual(moment([2016, DEC, 1]).format());
|
||||
});
|
||||
|
||||
it('should respect month length differences when adding months', () => {
|
||||
expect(adapter.addCalendarMonths(moment([2017, JAN, 31]), 1).format())
|
||||
.toEqual(moment([2017, FEB, 28]).format());
|
||||
expect(adapter.addCalendarMonths(moment([2017, MAR, 31]), -1).format())
|
||||
.toEqual(moment([2017, FEB, 28]).format());
|
||||
});
|
||||
|
||||
it('should add days', () => {
|
||||
expect(adapter.addCalendarDays(moment([2017, JAN, 1]), 1).format())
|
||||
.toEqual(moment([2017, JAN, 2]).format());
|
||||
expect(adapter.addCalendarDays(moment([2017, JAN, 1]), -1).format())
|
||||
.toEqual(moment([2016, DEC, 31]).format());
|
||||
});
|
||||
|
||||
it('should clone', () => {
|
||||
let date = moment([2017, JAN, 1]);
|
||||
expect(adapter.clone(date).format()).toEqual(date.format());
|
||||
expect(adapter.clone(date)).not.toBe(date);
|
||||
});
|
||||
|
||||
it('should compare dates', () => {
|
||||
expect(adapter.compareDate(moment([2017, JAN, 1]), moment([2017, JAN, 2]))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(moment([2017, JAN, 1]), moment([2017, FEB, 1]))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(moment([2017, JAN, 1]), moment([2018, JAN, 1]))).toBeLessThan(0);
|
||||
expect(adapter.compareDate(moment([2017, JAN, 1]), moment([2017, JAN, 1]))).toBe(0);
|
||||
expect(adapter.compareDate(moment([2018, JAN, 1]), moment([2017, JAN, 1]))).toBeGreaterThan(0);
|
||||
expect(adapter.compareDate(moment([2017, FEB, 1]), moment([2017, JAN, 1]))).toBeGreaterThan(0);
|
||||
expect(adapter.compareDate(moment([2017, JAN, 2]), moment([2017, JAN, 1]))).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should clamp date at lower bound', () => {
|
||||
expect(adapter.clampDate(
|
||||
moment([2017, JAN, 1]), moment([2018, JAN, 1]), moment([2019, JAN, 1])))
|
||||
.toEqual(moment([2018, JAN, 1]));
|
||||
});
|
||||
|
||||
it('should clamp date at upper bound', () => {
|
||||
expect(adapter.clampDate(
|
||||
moment([2020, JAN, 1]), moment([2018, JAN, 1]), moment([2019, JAN, 1])))
|
||||
.toEqual(moment([2019, JAN, 1]));
|
||||
});
|
||||
|
||||
it('should clamp date already within bounds', () => {
|
||||
expect(adapter.clampDate(
|
||||
moment([2018, FEB, 1]), moment([2018, JAN, 1]), moment([2019, JAN, 1])))
|
||||
.toEqual(moment([2018, FEB, 1]));
|
||||
});
|
||||
|
||||
it('should count today as a valid date instance', () => {
|
||||
let d = moment();
|
||||
expect(adapter.isValid(d)).toBe(true);
|
||||
expect(adapter.isDateInstance(d)).toBe(true);
|
||||
});
|
||||
|
||||
it('should count an invalid date as an invalid date instance', () => {
|
||||
let d = moment(NaN);
|
||||
expect(adapter.isValid(d)).toBe(false);
|
||||
expect(adapter.isDateInstance(d)).toBe(true);
|
||||
});
|
||||
|
||||
it('should count a string as not a date instance', () => {
|
||||
let d = '1/1/2017';
|
||||
expect(adapter.isDateInstance(d)).toBe(false);
|
||||
});
|
||||
|
||||
it('should count a Date as not a date instance', () => {
|
||||
let d = new Date();
|
||||
expect(adapter.isDateInstance(d)).toBe(false);
|
||||
});
|
||||
|
||||
it('setLocale should not modify global moment locale', () => {
|
||||
expect(moment.locale()).toBe('en');
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(moment.locale()).toBe('en');
|
||||
});
|
||||
|
||||
it('returned Moments should have correct locale', () => {
|
||||
adapter.setLocale('ja-JP');
|
||||
expect(adapter.createDate(2017, JAN, 1).locale()).toBe('ja');
|
||||
expect(adapter.today().locale()).toBe('ja');
|
||||
expect(adapter.clone(moment()).locale()).toBe('ja');
|
||||
expect(adapter.parse('1/1/2017', 'MM/DD/YYYY')!.locale()).toBe('ja');
|
||||
expect(adapter.addCalendarDays(moment(), 1).locale()).toBe('ja');
|
||||
expect(adapter.addCalendarMonths(moment(), 1).locale()).toBe('ja');
|
||||
expect(adapter.addCalendarYears(moment(), 1).locale()).toBe('ja');
|
||||
});
|
||||
|
||||
it('should not change locale of Moments passed as params', () => {
|
||||
let date = moment();
|
||||
expect(date.locale()).toBe('en');
|
||||
adapter.setLocale('ja-JP');
|
||||
adapter.getYear(date);
|
||||
adapter.getMonth(date);
|
||||
adapter.getDate(date);
|
||||
adapter.getDayOfWeek(date);
|
||||
adapter.getYearName(date);
|
||||
adapter.getNumDaysInMonth(date);
|
||||
adapter.clone(date);
|
||||
adapter.parse(date, 'MM/DD/YYYY');
|
||||
adapter.format(date, 'MM/DD/YYYY');
|
||||
adapter.addCalendarDays(date, 1);
|
||||
adapter.addCalendarMonths(date, 1);
|
||||
adapter.addCalendarYears(date, 1);
|
||||
adapter.getISODateString(date);
|
||||
adapter.isDateInstance(date);
|
||||
adapter.isValid(date);
|
||||
expect(date.locale()).toBe('en');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MomentDateAdapter with MAT_DATE_LOCALE override', () => {
|
||||
let adapter: MomentDateAdapter;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MomentDateModule],
|
||||
providers: [{provide: MAT_DATE_LOCALE, useValue: 'ja-JP'}]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([DateAdapter], (d: MomentDateAdapter) => {
|
||||
adapter = d;
|
||||
}));
|
||||
|
||||
it('should take the default locale id from the MAT_DATE_LOCALE injection token', () => {
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'll')).toEqual('2017年1月2日');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MomentDateAdapter with LOCALE_ID override', () => {
|
||||
let adapter: MomentDateAdapter;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [MomentDateModule],
|
||||
providers: [{provide: LOCALE_ID, useValue: 'fr'}]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(inject([DateAdapter], (d: MomentDateAdapter) => {
|
||||
adapter = d;
|
||||
}));
|
||||
|
||||
it('should take the default locale id from the LOCALE_ID injection token', () => {
|
||||
expect(adapter.format(moment([2017, JAN, 2]), 'll')).toEqual('2 janv. 2017');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,184 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {Inject, Injectable, Optional} from '@angular/core';
|
||||
import {DateAdapter, MAT_DATE_LOCALE} from '@angular/material';
|
||||
|
||||
// Depending on whether rollup is used, moment needs to be imported differently.
|
||||
// Since Moment.js doesn't have a default export, we normally need to import using the `* as`
|
||||
// syntax. However, rollup creates a synthetic default module and we thus need to import it using
|
||||
// the `default as` syntax.
|
||||
// TODO(mmalerba): See if we can clean this up at some point.
|
||||
import {default as _rollupMoment, Moment} from 'moment';
|
||||
import * as _moment from 'moment';
|
||||
const moment = _rollupMoment || _moment;
|
||||
|
||||
|
||||
/** Creates an array and fills it with values. */
|
||||
function range<T>(length: number, valueFunction: (index: number) => T): T[] {
|
||||
const valuesArray = Array(length);
|
||||
for (let i = 0; i < length; i++) {
|
||||
valuesArray[i] = valueFunction(i);
|
||||
}
|
||||
return valuesArray;
|
||||
}
|
||||
|
||||
|
||||
/** Adapts Moment.js Dates for use with Angular Material. */
|
||||
@Injectable()
|
||||
export class MomentDateAdapter extends DateAdapter<Moment> {
|
||||
// Note: all of the methods that accept a `Moment` input parameter immediately call `this.clone`
|
||||
// on it. This is to ensure that we're working with a `Moment` that has the correct locale setting
|
||||
// while avoiding mutating the original object passed to us. Just calling `.locale(...)` on the
|
||||
// input would mutate the object.
|
||||
|
||||
private _localeData: {
|
||||
firstDayOfWeek: number,
|
||||
longMonths: string[],
|
||||
shortMonths: string[],
|
||||
dates: string[],
|
||||
longDaysOfWeek: string[],
|
||||
shortDaysOfWeek: string[],
|
||||
narrowDaysOfWeek: string[]
|
||||
};
|
||||
|
||||
constructor(@Optional() @Inject(MAT_DATE_LOCALE) dateLocale: string) {
|
||||
super();
|
||||
this.setLocale(dateLocale || moment.locale());
|
||||
}
|
||||
|
||||
setLocale(locale: string) {
|
||||
super.setLocale(locale);
|
||||
|
||||
let momentLocaleData = moment.localeData(locale);
|
||||
this._localeData = {
|
||||
firstDayOfWeek: momentLocaleData.firstDayOfWeek(),
|
||||
longMonths: momentLocaleData.months(),
|
||||
shortMonths: momentLocaleData.monthsShort(),
|
||||
dates: range(31, (i) => this.createDate(2017, 0, i + 1).format('D')),
|
||||
longDaysOfWeek: momentLocaleData.weekdays(),
|
||||
shortDaysOfWeek: momentLocaleData.weekdaysShort(),
|
||||
narrowDaysOfWeek: momentLocaleData.weekdaysMin(),
|
||||
};
|
||||
}
|
||||
|
||||
getYear(date: Moment): number {
|
||||
return this.clone(date).year();
|
||||
}
|
||||
|
||||
getMonth(date: Moment): number {
|
||||
return this.clone(date).month();
|
||||
}
|
||||
|
||||
getDate(date: Moment): number {
|
||||
return this.clone(date).date();
|
||||
}
|
||||
|
||||
getDayOfWeek(date: Moment): number {
|
||||
return this.clone(date).day();
|
||||
}
|
||||
|
||||
getMonthNames(style: 'long' | 'short' | 'narrow'): string[] {
|
||||
// Moment.js doesn't support narrow month names, so we just use short if narrow is requested.
|
||||
return style == 'long' ? this._localeData.longMonths : this._localeData.shortMonths;
|
||||
}
|
||||
|
||||
getDateNames(): string[] {
|
||||
return this._localeData.dates;
|
||||
}
|
||||
|
||||
getDayOfWeekNames(style: 'long' | 'short' | 'narrow'): string[] {
|
||||
if (style == 'long') {
|
||||
return this._localeData.longDaysOfWeek;
|
||||
}
|
||||
if (style == 'short') {
|
||||
return this._localeData.shortDaysOfWeek;
|
||||
}
|
||||
return this._localeData.narrowDaysOfWeek;
|
||||
}
|
||||
|
||||
getYearName(date: Moment): string {
|
||||
return this.clone(date).format('YYYY');
|
||||
}
|
||||
|
||||
getFirstDayOfWeek(): number {
|
||||
return this._localeData.firstDayOfWeek;
|
||||
}
|
||||
|
||||
getNumDaysInMonth(date: Moment): number {
|
||||
return this.clone(date).daysInMonth();
|
||||
}
|
||||
|
||||
clone(date: Moment): Moment {
|
||||
return date.clone().locale(this.locale);
|
||||
}
|
||||
|
||||
createDate(year: number, month: number, date: number): Moment {
|
||||
// Moment.js will create an invalid date if any of the components are out of bounds, but we
|
||||
// explicitly check each case so we can throw more descriptive errors.
|
||||
if (month < 0 || month > 11) {
|
||||
throw Error(`Invalid month index "${month}". Month index has to be between 0 and 11.`);
|
||||
}
|
||||
|
||||
if (date < 1) {
|
||||
throw Error(`Invalid date "${date}". Date has to be greater than 0.`);
|
||||
}
|
||||
|
||||
let result = moment({year, month, date}).locale(this.locale);
|
||||
|
||||
// If the result isn't valid, the date must have been out of bounds for this month.
|
||||
if (!result.isValid()) {
|
||||
throw Error(`Invalid date "${date}" for month with index "${month}".`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
today(): Moment {
|
||||
return moment().locale(this.locale);
|
||||
}
|
||||
|
||||
parse(value: any, parseFormat: string | string[]): Moment | null {
|
||||
if (value && typeof value == 'string') {
|
||||
return moment(value, parseFormat, this.locale);
|
||||
}
|
||||
return value ? moment(value).locale(this.locale) : null;
|
||||
}
|
||||
|
||||
format(date: Moment, displayFormat: string): string {
|
||||
date = this.clone(date);
|
||||
if (!this.isValid(date)) {
|
||||
throw Error('MomentDateAdapter: Cannot format invalid date.');
|
||||
}
|
||||
return date.format(displayFormat);
|
||||
}
|
||||
|
||||
addCalendarYears(date: Moment, years: number): Moment {
|
||||
return this.clone(date).add({years});
|
||||
}
|
||||
|
||||
addCalendarMonths(date: Moment, months: number): Moment {
|
||||
return this.clone(date).add({months});
|
||||
}
|
||||
|
||||
addCalendarDays(date: Moment, days: number): Moment {
|
||||
return this.clone(date).add({days});
|
||||
}
|
||||
|
||||
getISODateString(date: Moment): string {
|
||||
return this.clone(date).format();
|
||||
}
|
||||
|
||||
isDateInstance(obj: any): boolean {
|
||||
return moment.isMoment(obj);
|
||||
}
|
||||
|
||||
isValid(date: Moment): boolean {
|
||||
return this.clone(date).isValid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {MdDateFormats} from '@angular/material';
|
||||
|
||||
|
||||
export const MD_MOMENT_DATE_FORMATS: MdDateFormats = {
|
||||
parse: {
|
||||
dateInput: 'l',
|
||||
},
|
||||
display: {
|
||||
dateInput: 'l',
|
||||
monthYearLabel: 'MMM YYYY',
|
||||
dateA11yLabel: 'LL',
|
||||
monthYearA11yLabel: 'MMMM YYYY',
|
||||
},
|
||||
};
|
||||
@@ -1,9 +0,0 @@
|
||||
import {_momentAdapter} from './public_api';
|
||||
|
||||
describe('moment-adapter', () => {
|
||||
|
||||
it('should be tested when running the tests', () => {
|
||||
expect(_momentAdapter).toBe(0);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -17,7 +17,8 @@
|
||||
"homepage": "https://github.com/angular/material2#readme",
|
||||
"peerDependencies": {
|
||||
"@angular/material": "0.0.0-PLACEHOLDER",
|
||||
"@angular/core": "^4.0.0"
|
||||
"@angular/core": "^4.0.0",
|
||||
"moment": "^2.18.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"tslib": "^1.7.1"
|
||||
|
||||
@@ -6,4 +6,4 @@
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const _momentAdapter = 0;
|
||||
export * from './adapter';
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// needs to be ES2015 since the build process will create FESM bundles using rollup.
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Needed for Moment.js since it doesn't have a default export.
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"baseUrl": ".",
|
||||
"declaration": true,
|
||||
"stripInternal": false,
|
||||
|
||||
@@ -20,6 +20,7 @@ System.config({
|
||||
map: {
|
||||
'rxjs': 'node:rxjs',
|
||||
'main': 'main.js',
|
||||
'moment': 'node:moment/min/moment-with-locales.min.js',
|
||||
|
||||
// Angular specific mappings.
|
||||
'@angular/core': 'node:@angular/core/bundles/core.umd.js',
|
||||
|
||||
@@ -26,6 +26,7 @@ module.exports = (config) => {
|
||||
{pattern: 'node_modules/zone.js/dist/fake-async-test.js', included: true, watched: false},
|
||||
{pattern: 'node_modules/hammerjs/hammer.min.js', included: true, watched: false},
|
||||
{pattern: 'node_modules/hammerjs/hammer.min.js.map', included: false, watched: false},
|
||||
{pattern: 'node_modules/moment/min/moment-with-locales.min.js', included: true, watched: false},
|
||||
|
||||
// Include all Angular dependencies
|
||||
{pattern: 'node_modules/@angular/**/*', included: false, watched: false},
|
||||
|
||||
@@ -18,7 +18,7 @@ const tsconfigFile = join(demoAppOut, 'tsconfig-aot.json');
|
||||
/** Builds the demo-app and material. To be able to run NGC, apply the metadata workaround. */
|
||||
task('aot:deps', sequenceTask(
|
||||
'build:devapp',
|
||||
['material:build-release', 'cdk:build-release'],
|
||||
['material:build-release', 'cdk:build-release', 'material-moment-adapter:build-release'],
|
||||
'aot:copy-release'
|
||||
));
|
||||
|
||||
@@ -27,6 +27,8 @@ task('aot:deps', sequenceTask(
|
||||
task('aot:copy-release', () => {
|
||||
copySync(join(releasesDir, 'material'), join(demoAppOut, 'material'));
|
||||
copySync(join(releasesDir, 'cdk'), join(demoAppOut, 'cdk'));
|
||||
copySync(
|
||||
join(releasesDir, 'material-moment-adapter'), join(demoAppOut, 'material-moment-adapter'));
|
||||
});
|
||||
|
||||
/** Build the demo-app and a release to confirm that the library is AOT-compatible. */
|
||||
|
||||
@@ -21,7 +21,7 @@ const materialOutPath = join(outputDir, 'packages', 'material');
|
||||
|
||||
/** Array of vendors that are required to serve the demo-app. */
|
||||
const appVendors = [
|
||||
'@angular', 'systemjs', 'zone.js', 'rxjs', 'hammerjs', 'core-js', 'web-animations-js'
|
||||
'@angular', 'systemjs', 'zone.js', 'rxjs', 'hammerjs', 'core-js', 'web-animations-js', 'moment',
|
||||
];
|
||||
|
||||
/** Glob that matches all required vendors for the demo-app. */
|
||||
|
||||
@@ -130,7 +130,9 @@ export function buildAppTask(appName: string) {
|
||||
.filter(taskName => gulp.hasTask(taskName));
|
||||
|
||||
return sequenceTask(
|
||||
'material:clean-build',
|
||||
// Build all required packages for serving the devapp by just building the moment-adapter
|
||||
// package. All dependencies of that package (material, cdk) will be built automatically.
|
||||
'material-moment-adapter:clean-build',
|
||||
[...buildTasks]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ const rollupCdkEntryPoints = cdkSecondaryEntryPoints.reduce((globals: any, entry
|
||||
/** Map of globals that are used inside of the different packages. */
|
||||
export const rollupGlobals = {
|
||||
'tslib': 'tslib',
|
||||
'moment': 'moment',
|
||||
|
||||
'@angular/animations': 'ng.animations',
|
||||
'@angular/core': 'ng.core',
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
// IDEs and TSLint. For IDEs it ensures that `experimentalDecorator` warnings are not showing up.
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Needed for Moment.js since it doesn't have a default export.
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"rootDir": ".",
|
||||
"experimentalDecorators": true,
|
||||
"module": "es2015",
|
||||
@@ -17,6 +19,7 @@
|
||||
"paths": {
|
||||
"@angular/material": ["./src/lib/public_api.ts"],
|
||||
"@angular/cdk/*": ["./src/cdk/*"],
|
||||
"@angular/material-moment-adapter": ["./src/material-moment-adapter"],
|
||||
"@angular/material-examples": ["./src/material-examples"],
|
||||
"material2-build-tools": ["./tools/package-tools"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user