feat(datepicker): @Output for year and month selected in multiyear/year (#9678)

* feat(datepicker): @Output of year and month selected in multiyear/year
views

* address @mmalerba's comments

* address comments

* add types to some outputs

* fix wrong parameter type

* restore accidentally removed object

* example and docs text

* change emitted values to objects instead of just numbers

* change datepicker.md and switch back tslint configs

* fix nit

* Remove changes in demo app
This commit is contained in:
Juliano
2018-02-06 15:32:23 -02:00
committed by mmalerba
parent eebfce4247
commit c2e108e0bf
20 changed files with 334 additions and 27 deletions
+4 -4
View File
@@ -11265,7 +11265,7 @@
"q": "1.5.1",
"sauce-connect-launcher": "1.2.3",
"saucelabs": "1.4.0",
"wd": "1.4.1"
"wd": "1.5.0"
},
"dependencies": {
"q": {
@@ -18067,9 +18067,9 @@
"dev": true
},
"wd": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/wd/-/wd-1.4.1.tgz",
"integrity": "sha512-C0wWd2X4SWWcyx5qxaixiZE4Vb07sl0yDfWHPeml8lDHSbmI9erE9BmTHIqOGoDxGgJ3/hkFmODQ7ZLKiF8+8Q==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/wd/-/wd-1.5.0.tgz",
"integrity": "sha512-e/KpzTlhtSG3Ek0AcRz4G6PhxGsc53Nro+GkI1er9p05tWQ7W9dpGZR5SqQzGUqvbaqJCThDSAGaY7LHgi6MiA==",
"dev": true,
"requires": {
"archiver": "1.3.0",
+2 -2
View File
@@ -93,7 +93,7 @@
<input matInput [matDatepicker]="datePicker1" [(ngModel)]="date" [min]="minDate" [max]="maxDate"
[matDatepickerFilter]="filterOdd ? dateFilter : null" disabled>
<mat-datepicker #datePicker1 [touchUi]="touch" [startAt]="startAt"
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
</mat-form-field>
</p>
@@ -121,7 +121,7 @@
<input matInput disabled [matDatepicker]="datePicker3" [(ngModel)]="date" [min]="minDate"
[max]="maxDate" [matDatepickerFilter]="filterOdd ? dateFilter : null">
<mat-datepicker #datePicker3 [touchUi]="touch" [disabled]="false" [startAt]="startAt"
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
[startView]="yearView ? 'year' : 'month'"></mat-datepicker>
</mat-form-field>
</p>
@@ -1,3 +1,4 @@
mat-calendar {
width: 300px;
}
+3 -4
View File
@@ -8,8 +8,7 @@
import {ChangeDetectionStrategy, Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatDatepickerInputEvent} from '@angular/material/datepicker';
import {MatDatepickerInputEvent} from '@angular/material';
@Component({
moduleId: module.id,
@@ -31,11 +30,11 @@ export class DatepickerDemo {
lastDateInput: Date | null;
lastDateChange: Date | null;
dateCtrl = new FormControl();
dateFilter =
(date: Date) => !(date.getFullYear() % 2) && (date.getMonth() % 2) && !(date.getDate() % 2)
onDateInput = (e: MatDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
onDateChange = (e: MatDatepickerInputEvent<Date>) => this.lastDateChange = e.value;
dateCtrl = new FormControl();
}
+1 -1
View File
@@ -31,7 +31,7 @@ import {AccessibilityDemoModule} from './a11y/a11y-module';
],
entryComponents: [
EntryApp,
],
]
})
export class DemoAppModule {
constructor(private _appRef: ApplicationRef) { }
+2
View File
@@ -36,6 +36,7 @@
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
(monthSelected)="_monthSelectedInYearView($event)"
(selectedChange)="_goToDateInView($event, 'month')">
</mat-year-view>
@@ -44,6 +45,7 @@
[activeDate]="_activeDate"
[selected]="selected"
[dateFilter]="_dateFilterForViews"
(yearSelected)="_yearSelectedInMultiYearView($event)"
(selectedChange)="_goToDateInView($event, 'year')">
</mat-multi-year-view>
</div>
+43 -1
View File
@@ -196,6 +196,40 @@ describe('MatCalendar', () => {
expect(testComponent.selected).toEqual(new Date(2017, JAN, 31));
});
it('should emit the selected month on cell clicked in year view', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('year');
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
const normalizedMonth: Date = fixture.componentInstance.selectedMonth;
expect(normalizedMonth.getMonth()).toEqual(0);
});
it('should emit the selected year on cell clicked in multiyear view', () => {
periodButton.click();
fixture.detectChanges();
expect(calendarInstance._currentView).toBe('multi-year');
expect(calendarInstance._activeDate).toEqual(new Date(2017, JAN, 31));
(calendarElement.querySelector('.mat-calendar-body-active') as HTMLElement).click();
fixture.detectChanges();
const normalizedYear: Date = fixture.componentInstance.selectedYear;
expect(normalizedYear.getFullYear()).toEqual(2017);
});
it('should re-render when the i18n labels have changed',
inject([MatDatepickerIntl], (intl: MatDatepickerIntl) => {
const button = fixture.debugElement.nativeElement
@@ -916,10 +950,18 @@ describe('MatCalendar', () => {
@Component({
template: `<mat-calendar [startAt]="startDate" [(selected)]="selected"></mat-calendar>`
template: `
<mat-calendar
[startAt]="startDate"
[(selected)]="selected"
(yearSelected)="selectedYear=$event"
(monthSelected)="selectedMonth=$event">
</mat-calendar>`
})
class StandardCalendar {
selected: Date;
selectedYear: Date;
selectedMonth: Date;
startDate = new Date(2017, JAN, 31);
}
+22
View File
@@ -107,6 +107,18 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
/** Emits when the currently selected date changes. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits the year chosen in multiyear view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits the month chosen in year view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
/** Emits when any date is selected. */
@Output() readonly _userSelection: EventEmitter<void> = new EventEmitter<void>();
@@ -228,6 +240,16 @@ export class MatCalendar<D> implements AfterContentInit, OnDestroy, OnChanges {
}
}
/** Handles year selection in the multiyear view. */
_yearSelectedInMultiYearView(normalizedYear: D) {
this.yearSelected.emit(normalizedYear);
}
/** Handles month selection in the year view. */
_monthSelectedInYearView(normalizedMonth: D) {
this.monthSelected.emit(normalizedMonth);
}
_userSelected(): void {
this._userSelection.emit();
}
@@ -8,5 +8,7 @@
[dateFilter]="datepicker._dateFilter"
[selected]="datepicker._selected"
(selectedChange)="datepicker._select($event)"
(yearSelected)="datepicker._selectYear($event)"
(monthSelected)="datepicker._selectMonth($event)"
(_userSelection)="datepicker.close()">
</mat-calendar>
+25
View File
@@ -51,6 +51,31 @@ year containing the `startAt` date.
<!-- example(datepicker-start-view) -->
#### Watching the views for changes on selected years and months
When a year or a month is selected in `multi-year` and `year` views respecively, the `yearSelected`
and `monthSelected` outputs emit a normalized date representing the chosen year or month. By
"normalized" we mean that the dates representing a year will have their month set to January and
their day set to the 1st. Dates representing months will have their day set to the 1st of the
month. For example, if `<mat-datepicker>` is configured to work with javascript native Date
objects, the `yearSelected` will emit `new Date(2017, 0, 1)` if the user selects 2017 in
`multi-year` view. Similarly, `monthSelected` will emit `new Date(2017, 1, 0)` if the user
selects **February** in `year` view and the current date value of the connected `<input>` was
something like `new Date(2017, MM, dd)` (the month and day are irrelevant in this case).
Notice that the emitted value does not affect the current value in the connected `<input>`, which
is only bound to the selection made in the `month` view. So if the end user closes the calendar
after choosing a year in `multi-view` mode (by pressing the `ESC` key, for example), the selected
year, emitted by `yearSelected` output, will not cause any change in the value of the date in the
associated `<input>`.
The following example uses `yearSelected` and `monthSelected` outputs to emulate a month and year
picker (if you're not familiar with the usage of `MomentDateAdapter` and `MAT_DATE_FORMATS`
you can [read more about them](#choosing-a-date-implementation-and-date-format-settings) below in
this document to fully understand the example).
<!-- example(datepicker-views-selection) -->
### Setting the selected date
The type of values that the datepicker expects depends on the type of `DateAdapter` provided in your
+97 -7
View File
@@ -433,12 +433,12 @@ describe('MatDatepicker', () => {
});
});
describe('datepicker with startView', () => {
let fixture: ComponentFixture<DatepickerWithStartView>;
let testComponent: DatepickerWithStartView;
describe('datepicker with startView set to year', () => {
let fixture: ComponentFixture<DatepickerWithStartViewYear>;
let testComponent: DatepickerWithStartViewYear;
beforeEach(fakeAsync(() => {
fixture = createComponent(DatepickerWithStartView, [MatNativeDateModule]);
fixture = createComponent(DatepickerWithStartViewYear, [MatNativeDateModule]);
fixture.detectChanges();
testComponent = fixture.componentInstance;
@@ -461,6 +461,73 @@ describe('MatDatepicker', () => {
expect(firstCalendarCell.textContent)
.toBe('JAN', 'Expected the calendar to be in year-view');
});
it('should fire yearSelected when user selects calendar year in year view',
fakeAsync(() => {
spyOn(testComponent, 'onYearSelection');
expect(testComponent.onYearSelection).not.toHaveBeenCalled();
testComponent.datepicker.open();
fixture.detectChanges();
const cells = document.querySelectorAll('.mat-calendar-body-cell');
dispatchMouseEvent(cells[0], 'click');
fixture.detectChanges();
flush();
expect(testComponent.onYearSelection).toHaveBeenCalled();
})
);
});
describe('datepicker with startView set to multiyear', () => {
let fixture: ComponentFixture<DatepickerWithStartViewMultiYear>;
let testComponent: DatepickerWithStartViewMultiYear;
beforeEach(fakeAsync(() => {
fixture = createComponent(DatepickerWithStartViewMultiYear, [MatNativeDateModule]);
fixture.detectChanges();
testComponent = fixture.componentInstance;
spyOn(testComponent, 'onMultiYearSelection');
}));
afterEach(fakeAsync(() => {
testComponent.datepicker.close();
fixture.detectChanges();
flush();
}));
it('should start at the specified view', () => {
testComponent.datepicker.open();
fixture.detectChanges();
const firstCalendarCell = document.querySelector('.mat-calendar-body-cell')!;
// When the calendar is in year view, the first cell should be for a month rather than
// for a date.
expect(firstCalendarCell.textContent)
.toBe('2016', 'Expected the calendar to be in multi-year-view');
});
it('should fire yearSelected when user selects calendar year in multiyear view',
fakeAsync(() => {
expect(testComponent.onMultiYearSelection).not.toHaveBeenCalled();
testComponent.datepicker.open();
fixture.detectChanges();
const cells = document.querySelectorAll('.mat-calendar-body-cell');
dispatchMouseEvent(cells[0], 'click');
fixture.detectChanges();
flush();
expect(testComponent.onMultiYearSelection).toHaveBeenCalled();
})
);
});
describe('datepicker with ngModel', () => {
@@ -996,7 +1063,8 @@ describe('MatDatepicker', () => {
expect(testComponent.onDateChange).toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).toHaveBeenCalled();
}));
})
);
});
describe('with ISO 8601 strings as input', () => {
@@ -1264,12 +1332,29 @@ class DatepickerWithStartAt {
@Component({
template: `
<input [matDatepicker]="d" [value]="date">
<mat-datepicker #d startView="year"></mat-datepicker>
<mat-datepicker #d startView="year" (monthSelected)="onYearSelection()"></mat-datepicker>
`,
})
class DatepickerWithStartView {
class DatepickerWithStartViewYear {
date = new Date(2020, JAN, 1);
@ViewChild('d') datepicker: MatDatepicker<Date>;
onYearSelection() {}
}
@Component({
template: `
<input [matDatepicker]="d" [value]="date">
<mat-datepicker #d startView="multi-year"
(yearSelected)="onMultiYearSelection()"></mat-datepicker>
`,
})
class DatepickerWithStartViewMultiYear {
date = new Date(2020, JAN, 1);
@ViewChild('d') datepicker: MatDatepicker<Date>;
onMultiYearSelection() {}
}
@@ -1314,6 +1399,7 @@ class DatepickerWithToggle {
touchUI = true;
}
@Component({
template: `
<input [matDatepicker]="d">
@@ -1325,6 +1411,7 @@ class DatepickerWithToggle {
})
class DatepickerWithCustomIcon {}
@Component({
template: `
<mat-form-field>
@@ -1387,6 +1474,7 @@ class DatepickerWithChangeAndInputEvents {
onDateInput() {}
}
@Component({
template: `
<input [matDatepicker]="d" [(ngModel)]="date">
@@ -1399,6 +1487,7 @@ class DatepickerWithi18n {
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}
@Component({
template: `
<input [matDatepicker]="d" [(ngModel)]="value" [min]="min" [max]="max">
@@ -1414,6 +1503,7 @@ class DatepickerWithISOStrings {
@ViewChild(MatDatepickerInput) datepickerInput: MatDatepickerInput<Date>;
}
@Component({
template: `
<input [(ngModel)]="selected" [matDatepicker]="d">
+23
View File
@@ -164,6 +164,18 @@ export class MatDatepicker<D> implements OnDestroy {
*/
@Output() readonly selectedChanged: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits selected year in multiyear view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
/**
* Emits selected month in year view.
* This doesn't imply a change on the selected date.
*/
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
/** Classes to be passed to the date picker panel. Supports the same syntax as `ngClass`. */
@Input() panelClass: string | string[];
@@ -173,6 +185,7 @@ export class MatDatepicker<D> implements OnDestroy {
/** Emits when the datepicker has been closed. */
@Output('closed') closedStream: EventEmitter<void> = new EventEmitter<void>();
/** Whether the calendar is open. */
@Input()
get opened(): boolean { return this._opened; }
@@ -253,6 +266,16 @@ export class MatDatepicker<D> implements OnDestroy {
}
}
/** Emits the selected year in multiyear view */
_selectYear(normalizedYear: D): void {
this.yearSelected.emit(normalizedYear);
}
/** Emits selected month in year view */
_selectMonth(normalizedMonth: D): void {
this.monthSelected.emit(normalizedMonth);
}
/**
* Register an input with this datepicker.
* @param input The datepicker input to register with this datepicker.
+13 -1
View File
@@ -66,6 +66,16 @@ describe('MatMultiYearView', () => {
expect(selectedEl.innerHTML.trim()).toBe('2039');
});
it('should emit the selected year on cell clicked', () => {
let cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
(cellEls[1] as HTMLElement).click();
fixture.detectChanges();
const normalizedYear: Date = fixture.componentInstance.selectedYear;
expect(normalizedYear.getFullYear()).toEqual(2017);
});
it('should mark active date', () => {
let cellEls = multiYearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[1] as HTMLElement).innerText.trim()).toBe('2017');
@@ -98,11 +108,13 @@ describe('MatMultiYearView', () => {
@Component({
template: `
<mat-multi-year-view [activeDate]="date" [(selected)]="selected"></mat-multi-year-view>`,
<mat-multi-year-view [activeDate]="date" [(selected)]="selected"
(yearSelected)="selectedYear=$event"></mat-multi-year-view>`
})
class StandardMultiYearView {
date = new Date(2017, JAN, 1);
selected = new Date(2020, JAN, 1);
selectedYear: Date;
@ViewChild(MatYearView) yearView: MatYearView<Date>;
}
+4
View File
@@ -70,6 +70,9 @@ export class MatMultiYearView<D> implements AfterContentInit {
/** Emits when a new month is selected. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
/** Emits the selected year. This doesn't imply a change on the selected date */
@Output() readonly yearSelected: EventEmitter<D> = new EventEmitter<D>();
/** Grid of calendar cells representing the currently displayed years. */
_years: MatCalendarCell[][];
@@ -110,6 +113,7 @@ export class MatMultiYearView<D> implements AfterContentInit {
/** Handles when a new year is selected. */
_yearSelected(year: number) {
this.yearSelected.emit(this._dateAdapter.createDate(year, 0, 1));
let month = this._dateAdapter.getMonth(this.activeDate);
let daysInMonth =
this._dateAdapter.getNumDaysInMonth(this._dateAdapter.createDate(year, month, 1));
+13 -1
View File
@@ -71,6 +71,16 @@ describe('MatYearView', () => {
expect(selectedEl.innerHTML.trim()).toBe('DEC');
});
it('should emit the selected month on cell clicked', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
(cellEls[cellEls.length - 1] as HTMLElement).click();
fixture.detectChanges();
const normalizedMonth: Date = fixture.componentInstance.selectedMonth;
expect(normalizedMonth.getMonth()).toEqual(11);
});
it('should mark active date', () => {
let cellEls = yearViewNativeElement.querySelectorAll('.mat-calendar-body-cell');
expect((cellEls[0] as HTMLElement).innerText.trim()).toBe('JAN');
@@ -113,11 +123,13 @@ describe('MatYearView', () => {
@Component({
template: `
<mat-year-view [activeDate]="date" [(selected)]="selected"></mat-year-view>`,
<mat-year-view [activeDate]="date" [(selected)]="selected"
(monthSelected)="selectedMonth=$event"></mat-year-view>`
})
class StandardYearView {
date = new Date(2017, JAN, 5);
selected = new Date(2017, MAR, 10);
selectedMonth: Date;
@ViewChild(MatYearView) yearView: MatYearView<Date>;
}
+10 -2
View File
@@ -65,6 +65,9 @@ export class MatYearView<D> implements AfterContentInit {
/** Emits when a new month is selected. */
@Output() readonly selectedChange: EventEmitter<D> = new EventEmitter<D>();
/** Emits the selected month. This doesn't imply a change on the selected date */
@Output() readonly monthSelected: EventEmitter<D> = new EventEmitter<D>();
/** Grid of calendar cells representing the months of the year. */
_months: MatCalendarCell[][];
@@ -99,8 +102,13 @@ export class MatYearView<D> implements AfterContentInit {
/** Handles when a new month is selected. */
_monthSelected(month: number) {
let daysInMonth = this._dateAdapter.getNumDaysInMonth(
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1));
const normalizedDate =
this._dateAdapter.createDate(this._dateAdapter.getYear(this.activeDate), month, 1);
this.monthSelected.emit(normalizedDate);
const daysInMonth = this._dateAdapter.getNumDaysInMonth(normalizedDate);
this.selectedChange.emit(this._dateAdapter.createDate(
this._dateAdapter.getYear(this.activeDate), month,
Math.min(this._dateAdapter.getDate(this.activeDate), daysInMonth)));
@@ -0,0 +1 @@
/** No CSS for this example */
@@ -0,0 +1,6 @@
<mat-form-field>
<input matInput [matDatepicker]="dp" placeholder="Month and Year" [formControl]="date">
<mat-datepicker-toggle matSuffix [for]="dp"></mat-datepicker-toggle>
<mat-datepicker #dp (yearSelected)="chosenYearHandler($event)"
(monthSelected)="chosenMonthHandler($event, dp)"></mat-datepicker>
</mat-form-field>
@@ -0,0 +1,58 @@
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {MatDatepicker} from '@angular/material/datepicker';
import {MomentDateAdapter} from '@angular/material-moment-adapter';
import {DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE} from '@angular/material/core';
// 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.
import * as _moment from 'moment';
import {default as _rollupMoment, Moment} from 'moment';
const moment = _rollupMoment || _moment;
// See the Moment.js docs for the meaning of these formats:
// https://momentjs.com/docs/#/displaying/format/
export const MY_FORMATS = {
parse: {
dateInput: 'MM/YYYY',
},
display: {
dateInput: 'MM/YYYY',
monthYearLabel: 'MMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY',
},
};
/** @title Datepicker emulating a Year and month picker */
@Component({
selector: 'datepicker-views-selection-example',
templateUrl: 'datepicker-views-selection-example.html',
styleUrls: ['datepicker-views-selection-example.css'],
providers: [
// `MomentDateAdapter` can be automatically provided by importing `MomentDateModule` in your
// application's root module. We provide it at the component level here, due to limitations of
// our example generation script.
{provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE]},
{provide: MAT_DATE_FORMATS, useValue: MY_FORMATS},
],
})
export class DatepickerViewsSelectionExample {
date = new FormControl(moment());
chosenYearHandler(normalizedYear: Moment) {
const ctrlValue = this.date.value;
ctrlValue.year(normalizedYear.year());
this.date.setValue(ctrlValue);
}
chosenMonthHandler(normlizedMonth: Moment, datepicker: MatDatepicker<Moment>) {
const ctrlValue = this.date.value;
ctrlValue.month(normlizedMonth.month());
this.date.setValue(ctrlValue);
datepicker.close();
}
}
+4 -4
View File
@@ -30,21 +30,21 @@ import {
MatInputModule,
MatListModule,
MatMenuModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatSliderModule,
MatSidenavModule,
MatSnackBarModule,
MatSortModule,
MatStepperModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
MatPaginatorModule,
MatSortModule,
MatTableModule
]
})
export class ExampleMaterialModule {}