Skip to content

Commit

Permalink
datepicker: create injectable for date formats and bundle it along wi…
Browse files Browse the repository at this point in the history
…th date adapter into MdNativeDateModule (#4296)

* New module structure for DateAdapter.

* pass through format options

* move date-formats to core/datetime

* don't subclass error

* add test for missing providers case
  • Loading branch information
mmalerba committed May 9, 2017
1 parent 9ab583a commit cb8a49d
Show file tree
Hide file tree
Showing 21 changed files with 494 additions and 405 deletions.
16 changes: 9 additions & 7 deletions src/demo-app/demo-app-module.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
import {NgModule, ApplicationRef} from '@angular/core';
import {ApplicationRef, NgModule} from '@angular/core';
import {BrowserModule} from '@angular/platform-browser';
import {HttpModule} from '@angular/http';
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
import {RouterModule} from '@angular/router';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {DemoApp, Home} from './demo-app/demo-app';
import {
MaterialModule,
OverlayContainer,
FullscreenOverlayContainer,
MaterialModule,
MdNativeDateModule,
MdSelectionModule,
OverlayContainer
} from '@angular/material';
import {DEMO_APP_ROUTES} from './demo-app/routes';
import {ProgressBarDemo} from './progress-bar/progress-bar-demo';
import {JazzDialog, ContentElementDialog, DialogDemo, IFrameDialog} from './dialog/dialog-demo';
import {ContentElementDialog, DialogDemo, IFrameDialog, JazzDialog} from './dialog/dialog-demo';
import {RippleDemo} from './ripple/ripple-demo';
import {IconDemo} from './icon/icon-demo';
import {GesturesDemo} from './gestures/gestures-demo';
Expand All @@ -27,18 +28,18 @@ import {ListDemo} from './list/list-demo';
import {BaselineDemo} from './baseline/baseline-demo';
import {GridListDemo} from './grid-list/grid-list-demo';
import {LiveAnnouncerDemo} from './live-announcer/live-announcer-demo';
import {OverlayDemo, SpagettiPanel, RotiniPanel} from './overlay/overlay-demo';
import {OverlayDemo, RotiniPanel, SpagettiPanel} from './overlay/overlay-demo';
import {SlideToggleDemo} from './slide-toggle/slide-toggle-demo';
import {ToolbarDemo} from './toolbar/toolbar-demo';
import {ButtonDemo} from './button/button-demo';
import {MdCheckboxDemoNestedChecklist, CheckboxDemo} from './checkbox/checkbox-demo';
import {CheckboxDemo, MdCheckboxDemoNestedChecklist} from './checkbox/checkbox-demo';
import {SelectDemo} from './select/select-demo';
import {SliderDemo} from './slider/slider-demo';
import {SidenavDemo} from './sidenav/sidenav-demo';
import {SnackBarDemo} from './snack-bar/snack-bar-demo';
import {PortalDemo, ScienceJoke} from './portal/portal-demo';
import {MenuDemo} from './menu/menu-demo';
import {TabsDemo, SunnyTabContent, RainyTabContent, FoggyTabContent} from './tabs/tabs-demo';
import {FoggyTabContent, RainyTabContent, SunnyTabContent, TabsDemo} from './tabs/tabs-demo';
import {PlatformDemo} from './platform/platform-demo';
import {AutocompleteDemo} from './autocomplete/autocomplete-demo';
import {InputDemo} from './input/input-demo';
Expand All @@ -55,6 +56,7 @@ import {DatepickerDemo} from './datepicker/datepicker-demo';
ReactiveFormsModule,
RouterModule.forRoot(DEMO_APP_ROUTES),
MaterialModule,
MdNativeDateModule,
MdSelectionModule,
],
declarations: [
Expand Down
3 changes: 0 additions & 3 deletions src/lib/core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import {OverlayModule} from './overlay/overlay-directives';
import {A11yModule} from './a11y/index';
import {MdSelectionModule} from './selection/index';
import {MdRippleModule} from './ripple/index';
import {DatetimeModule} from './datetime/index';


// RTL
Expand Down Expand Up @@ -135,7 +134,6 @@ export * from './datetime/index';
A11yModule,
MdOptionModule,
MdSelectionModule,
DatetimeModule,
],
exports: [
MdLineModule,
Expand All @@ -147,7 +145,6 @@ export * from './datetime/index';
A11yModule,
MdOptionModule,
MdSelectionModule,
DatetimeModule,
],
})
export class MdCoreModule {}
25 changes: 5 additions & 20 deletions src/lib/core/datetime/date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,6 @@ export abstract class DateAdapter<D> {
*/
abstract getYearName(date: D): string;

/**
* Gets the name for the month and year of the given date.
* @param date The date to get the month and year name for.
* @param monthStyle The naming style for the month
* (e.g. long = 'January', short = 'Jan', narrow = 'J').
* @returns The name of the month and year of the given date (e.g. 'Jan 2017').
*/
abstract getMonthYearName(date: D, monthStyle: 'long' | 'short' | 'narrow'): string;

/**
* Gets the first day of the week.
* @returns The first day of the week (0-indexed, 0 = Sunday).
Expand All @@ -80,13 +71,6 @@ export abstract class DateAdapter<D> {
*/
abstract getNumDaysInMonth(date: D): number;

/**
* Gets a set of default formats to use for displaying the date in different contexts.
* @returns An object with the following default formats:
* - date: The default format for showing just the date without any time information.
*/
abstract getDefaultFormats(): {date: any};

/**
* Clones the given date.
* @param date The date to clone
Expand All @@ -113,18 +97,19 @@ export abstract class DateAdapter<D> {
/**
* Parses a date from a value.
* @param value The value to parse.
* @param fmt The expected format of the value being parsed (type is implementation-dependent).
* @param parseFormat The expected format of the value being parsed
* (type is implementation-dependent).
* @returns The parsed date, or null if date could not be parsed.
*/
abstract parse(value: any, fmt?: any): D | null;
abstract parse(value: any, parseFormat: any): D | null;

/**
* Formats a date as a string.
* @param date The value to parse.
* @param fmt The format to use for the result string.
* @param displayFormat The format to use to display the date as a string.
* @returns The parsed date, or null if date could not be parsed.
*/
abstract format(date: D, fmt?: any): string;
abstract format(date: D, displayFormat: any): string;

/**
* Adds the given number of years to the date. Years are counted as if flipping 12 pages on the
Expand Down
15 changes: 15 additions & 0 deletions src/lib/core/datetime/date-formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {InjectionToken} from '@angular/core';


export type MdDateFormats = {
parse: {
dateInput: any
},
display: {
dateInput: any,
monthYearLabel: any,
}
};


export const MD_DATE_FORMATS = new InjectionToken<MdDateFormats>('md-date-formats');
11 changes: 10 additions & 1 deletion src/lib/core/datetime/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {NgModule} from '@angular/core';
import {DateAdapter} from './date-adapter';
import {NativeDateAdapter} from './native-date-adapter';
import {MD_DATE_FORMATS} from './date-formats';
import {MD_NATIVE_DATE_FORMATS} from './native-date-formats';


export * from './date-adapter';
Expand All @@ -10,4 +12,11 @@ export * from './native-date-adapter';
@NgModule({
providers: [{provide: DateAdapter, useClass: NativeDateAdapter}],
})
export class DatetimeModule {}
export class NativeDateModule {}


@NgModule({
imports: [NativeDateModule],
providers: [{provide: MD_DATE_FORMATS, useValue: MD_NATIVE_DATE_FORMATS}],
})
export class MdNativeDateModule {}
22 changes: 0 additions & 22 deletions src/lib/core/datetime/native-date-adapter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,32 +104,10 @@ describe('NativeDateAdapter', () => {
expect(adapter.getYearName(new Date(2017, JAN, 1))).toBe('2017年');
});

it('should get long month and year name', () => {
expect(adapter.getMonthYearName(new Date(2017, JAN, 1), 'long')).toBe('January 2017');
});

it('should get short month and year name', () => {
expect(adapter.getMonthYearName(new Date(2017, JAN, 1), 'short')).toBe('Jan 2017');
});

it('should get narrow month and year name', () => {
expect(adapter.getMonthYearName(new Date(2017, JAN, 1), 'narrow')).toBe('J 2017');
});

it('should get month and year name in a different locale', () => {
adapter.setLocale('ja-JP');
expect(adapter.getMonthYearName(new Date(2017, JAN, 1), 'long')).toBe('2017年1月');
});

it('should get first day of week', () => {
expect(adapter.getFirstDayOfWeek()).toBe(0);
});

it('should get default formats', () => {
let dtf = new Intl.DateTimeFormat('en-US', adapter.getDefaultFormats().date);
expect(dtf.format(new Date(2017, 1, 1))).toEqual('2/1/2017');
});

it('should create Date', () => {
expect(adapter.createDate(2017, JAN, 1)).toEqual(new Date(2017, JAN, 1));
});
Expand Down
25 changes: 3 additions & 22 deletions src/lib/core/datetime/native-date-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,6 @@ export class NativeDateAdapter extends DateAdapter<Date> {
return String(this.getYear(date));
}

getMonthYearName(date: Date, monthStyle: 'long' | 'short' | 'narrow'): string {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, {month: monthStyle, year: 'numeric'});
return dtf.format(date);
}
let monthName = this.getMonthNames(monthStyle)[this.getMonth(date)];
return `${monthName} ${this.getYear(date)}`;
}

getFirstDayOfWeek(): number {
// We can't tell using native JS Date what the first day of the week is, we default to Sunday.
return 0;
Expand All @@ -104,16 +95,6 @@ export class NativeDateAdapter extends DateAdapter<Date> {
this.getYear(date), this.getMonth(date) + 1, 0));
}

getDefaultFormats(): {date: Object} {
return {
date: {
year: 'numeric',
month: 'numeric',
day: 'numeric'
}
};
}

clone(date: Date): Date {
return this.createDate(this.getYear(date), this.getMonth(date), this.getDate(date));
}
Expand All @@ -140,16 +121,16 @@ export class NativeDateAdapter extends DateAdapter<Date> {
return new Date();
}

parse(value: any, fmt?: Object): Date | null {
parse(value: any, parseFormat: Object): Date | null {
// We have no way using the native JS Date to set the parse format or locale, so we ignore these
// parameters.
let timestamp = typeof value == 'number' ? value : Date.parse(value);
return isNaN(timestamp) ? null : new Date(timestamp);
}

format(date: Date, fmt?: Object): string {
format(date: Date, displayFormat: Object): string {
if (SUPPORTS_INTL_API) {
let dtf = new Intl.DateTimeFormat(this.locale, fmt);
let dtf = new Intl.DateTimeFormat(this.locale, displayFormat);
return dtf.format(date);
}
return date.toDateString();
Expand Down
12 changes: 12 additions & 0 deletions src/lib/core/datetime/native-date-formats.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import {MdDateFormats} from './date-formats';


export const MD_NATIVE_DATE_FORMATS: MdDateFormats = {
parse: {
dateInput: null,
},
display: {
dateInput: {year: 'numeric', month: 'numeric', day: 'numeric'},
monthYearLabel: {year: 'numeric', month: 'short'},
}
};
4 changes: 2 additions & 2 deletions src/lib/datepicker/calendar.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {By} from '@angular/platform-browser';
import {MdMonthView} from './month-view';
import {MdYearView} from './year-view';
import {MdCalendarBody} from './calendar-body';
import {DatetimeModule} from '../core/datetime/index';
import {
dispatchFakeEvent,
dispatchKeyboardEvent,
Expand All @@ -23,6 +22,7 @@ import {
UP_ARROW
} from '../core/keyboard/keycodes';
import {MdDatepickerIntl} from './datepicker-intl';
import {MdNativeDateModule} from '../core/datetime/index';


// When constructing a Date, the month is zero-based. This can be confusing, since people are
Expand All @@ -35,7 +35,7 @@ describe('MdCalendar', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
DatetimeModule,
MdNativeDateModule,
],
declarations: [
MdCalendar,
Expand Down
34 changes: 28 additions & 6 deletions src/lib/datepicker/calendar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import {
ChangeDetectionStrategy,
Component,
EventEmitter,
Inject,
Input,
Optional,
Output,
ViewEncapsulation
} from '@angular/core';
Expand All @@ -20,6 +22,8 @@ import {
} from '../core/keyboard/keycodes';
import {DateAdapter} from '../core/datetime/index';
import {MdDatepickerIntl} from './datepicker-intl';
import {createMissingDateImplError} from './datepicker-errors';
import {MD_DATE_FORMATS, MdDateFormats} from '../core/datetime/date-formats';


/**
Expand All @@ -41,7 +45,9 @@ export class MdCalendar<D> implements AfterContentInit {
/** A date representing the period (month or year) to start the calendar in. */
@Input()
get startAt(): D { return this._startAt; }
set startAt(value: D) { this._startAt = this._dateAdapter.parse(value); }
set startAt(value: D) {
this._startAt = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
}
private _startAt: D;

/** Whether the calendar should be started in month or year view. */
Expand All @@ -50,19 +56,25 @@ export class MdCalendar<D> implements AfterContentInit {
/** The currently selected date. */
@Input()
get selected(): D { return this._selected; }
set selected(value: D) { this._selected = this._dateAdapter.parse(value); }
set selected(value: D) {
this._selected = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
}
private _selected: D;

/** The minimum selectable date. */
@Input()
get minDate(): D { return this._minDate; }
set minDate(date: D) { this._minDate = this._dateAdapter.parse(date); }
set minDate(date: D) {
this._minDate = this._dateAdapter.parse(date, this._dateFormats.parse.dateInput);
}
private _minDate: D;

/** The maximum selectable date. */
@Input()
get maxDate(): D { return this._maxDate; }
set maxDate(date: D) { this._maxDate = this._dateAdapter.parse(date); }
set maxDate(date: D) {
this._maxDate = this._dateAdapter.parse(date, this._dateFormats.parse.dateInput);
}
private _maxDate: D;

/** A function used to filter which dates are selectable. */
Expand Down Expand Up @@ -95,7 +107,8 @@ export class MdCalendar<D> implements AfterContentInit {
/** The label for the current calendar view. */
get _periodButtonText(): string {
return this._monthView ?
this._dateAdapter.getMonthYearName(this._activeDate, 'short').toLocaleUpperCase() :
this._dateAdapter.format(this._activeDate, this._dateFormats.display.monthYearLabel)
.toLocaleUpperCase() :
this._dateAdapter.getYearName(this._activeDate);
}

Expand All @@ -113,7 +126,16 @@ export class MdCalendar<D> implements AfterContentInit {
return this._monthView ? this._intl.nextMonthLabel : this._intl.nextYearLabel;
}

constructor(private _dateAdapter: DateAdapter<D>, private _intl: MdDatepickerIntl) {}
constructor(private _intl: MdDatepickerIntl,
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() @Inject(MD_DATE_FORMATS) private _dateFormats: MdDateFormats) {
if (!this._dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}
if (!this._dateFormats) {
throw createMissingDateImplError('MD_DATE_FORMATS');
}
}

ngAfterContentInit() {
this._activeDate = this.startAt || this._dateAdapter.today();
Expand Down
6 changes: 6 additions & 0 deletions src/lib/datepicker/datepicker-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/** @docs-private */
export function createMissingDateImplError(provider: string) {
return new Error(
`MdDatepicker: No provider found for ${provider}. You must import one of the following` +
`modules at your application root: MdNativeDateModule, or provide a custom implementation.`);
}
Loading

0 comments on commit cb8a49d

Please sign in to comment.