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:
mmalerba
2017-09-12 13:24:23 -07:00
committed by GitHub
parent 1cccd4bda6
commit 9545427c73
22 changed files with 678 additions and 18 deletions
+1
View File
@@ -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.
+2
View File
@@ -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',
+2 -1
View File
@@ -11,7 +11,8 @@
"outDir": ".",
"paths": {
"@angular/material": ["./material"],
"@angular/cdk/*": ["./cdk/*"]
"@angular/cdk/*": ["./cdk/*"],
"@angular/material-moment-adapter": ["./material-moment-adapter"]
}
},
"files": [
+2 -1
View File
@@ -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": [
+8
View File
@@ -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();
}
/**
+8
View File
@@ -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);
});
});
+2 -1
View File
@@ -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"
+1 -1
View File
@@ -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,
+1
View File
@@ -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',
+1
View File
@@ -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},
+3 -1
View File
@@ -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. */
+1 -1
View File
@@ -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. */
+3 -1
View File
@@ -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]
);
}
+1
View File
@@ -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',
+3
View File
@@ -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"]
}