Skip to content

Commit 35eb294

Browse files
mmalerbakara
authored andcommitted
fix(datepicker): better support for input and change events (#4826)
1 parent 1cba2dc commit 35eb294

File tree

5 files changed

+152
-2
lines changed

5 files changed

+152
-2
lines changed

src/demo-app/datepicker/datepicker-demo.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ <h2>Result</h2>
3737
[min]="minDate"
3838
[max]="maxDate"
3939
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
40-
placeholder="Pick a date">
40+
placeholder="Pick a date"
41+
(dateInput)="onDateInput($event)"
42+
(dateChange)="onDateChange($event)">
4143
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMin')">Too early!</md-error>
4244
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMax')">Too late!</md-error>
4345
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerFilter')">Date unavailable!</md-error>
@@ -50,6 +52,9 @@ <h2>Result</h2>
5052
[startView]="yearView ? 'year' : 'month'">
5153
</md-datepicker>
5254
</p>
55+
<p>Last input: {{lastDateInput}}</p>
56+
<p>Last change: {{lastDateChange}}</p>
57+
<br>
5358
<p>
5459
<input #resultPickerModel2
5560
[mdDatepicker]="resultPicker2"

src/demo-app/datepicker/datepicker-demo.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {Component} from '@angular/core';
2+
import {MdDatepickerInputEvent} from '@angular/material';
23

34

45
@Component({
@@ -17,5 +18,11 @@ export class DatepickerDemo {
1718
maxDate: Date;
1819
startAt: Date;
1920
date: Date;
21+
lastDateInput: Date | null;
22+
lastDateChange: Date | null;
23+
2024
dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;
25+
26+
onDateInput = (e: MdDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
27+
onDateChange = (e: MdDatepickerInputEvent<Date>) => this.lastDateChange = e.value;
2128
}

src/lib/datepicker/datepicker-input.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Input,
1717
OnDestroy,
1818
Optional,
19+
Output,
1920
Renderer2
2021
} from '@angular/core';
2122
import {MdDatepicker} from './datepicker';
@@ -52,6 +53,21 @@ export const MD_DATEPICKER_VALIDATORS: any = {
5253
};
5354

5455

56+
/**
57+
* An event used for datepicker input and change events. We don't always have access to a native
58+
* input or change event because the event may have been triggered by the user clicking on the
59+
* calendar popup. For consistency, we always use MdDatepickerInputEvent instead.
60+
*/
61+
export class MdDatepickerInputEvent<D> {
62+
/** The new value for the target datepicker input. */
63+
value: D | null;
64+
65+
constructor(public target: MdDatepickerInput<D>, public targetElement: HTMLElement) {
66+
this.value = this.target.value;
67+
}
68+
}
69+
70+
5571
/** Directive used to connect an input to a MdDatepicker. */
5672
@Directive({
5773
selector: 'input[mdDatepicker], input[matDatepicker]',
@@ -63,9 +79,11 @@ export const MD_DATEPICKER_VALIDATORS: any = {
6379
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
6480
'[disabled]': 'disabled',
6581
'(input)': '_onInput($event.target.value)',
82+
'(change)': '_onChange()',
6683
'(blur)': '_onTouched()',
6784
'(keydown)': '_onKeydown($event)',
68-
}
85+
},
86+
exportAs: 'mdDatepickerInput',
6987
})
7088
export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAccessor, OnDestroy,
7189
Validator {
@@ -133,6 +151,12 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
133151
}
134152
private _disabled: boolean;
135153

154+
/** Emits when a `change` event is fired on this `<input>`. */
155+
@Output() dateChange = new EventEmitter<MdDatepickerInputEvent<D>>();
156+
157+
/** Emits when an `input` event is fired on this `<input>`. */
158+
@Output() dateInput = new EventEmitter<MdDatepickerInputEvent<D>>();
159+
136160
/** Emits when the value changes (either due to user input or programmatic change). */
137161
_valueChange = new EventEmitter<D|null>();
138162

@@ -188,6 +212,8 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
188212
this._datepicker.selectedChanged.subscribe((selected: D) => {
189213
this.value = selected;
190214
this._cvaOnChange(selected);
215+
this.dateInput.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
216+
this.dateChange.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
191217
});
192218
}
193219
}
@@ -245,5 +271,10 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
245271
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
246272
this._cvaOnChange(date);
247273
this._valueChange.emit(date);
274+
this.dateInput.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
275+
}
276+
277+
_onChange() {
278+
this.dateChange.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
248279
}
249280
}

src/lib/datepicker/datepicker.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,17 @@ Each validation property has a different error that can be checked:
8282
* A value that violates the `min` property will have a `mdDatepickerMin` error.
8383
* A value that violates the `max` property will have a `mdDatepickerMax` error.
8484
* A value that violates the `mdDatepickerFilter` property will have a `mdDatepickerFilter` error.
85+
86+
### Input and change events
87+
The input's native `input` and `change` events will only trigger due to user interaction with the
88+
input element; they will not fire when the user selects a date from the calendar popup. Because of
89+
this limitation, the datepicker input also has support for `dateInput` and `dateChange` events.
90+
These trigger when the user interacts with either the input or the popup.
91+
92+
```html
93+
<input [mdDatepicker]="d" (dateInput)="onInput($event)" (dateChange)="onChange($event)">
94+
<md-datepicker #d></md-datepicker>
95+
```
8596

8697
### Touch UI mode
8798
The datepicker normally opens as a popup under the input. However this is not ideal for touch

src/lib/datepicker/datepicker.spec.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ describe('MdDatepicker', () => {
2929
ReactiveFormsModule,
3030
],
3131
declarations: [
32+
DatepickerWithChangeAndInputEvents,
3233
DatepickerWithFilterAndValidation,
3334
DatepickerWithFormControl,
3435
DatepickerWithMinAndMaxValidation,
@@ -689,6 +690,81 @@ describe('MdDatepicker', () => {
689690
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
690691
});
691692
});
693+
694+
describe('datepicker with change and input events', () => {
695+
let fixture: ComponentFixture<DatepickerWithChangeAndInputEvents>;
696+
let testComponent: DatepickerWithChangeAndInputEvents;
697+
let inputEl: HTMLInputElement;
698+
699+
beforeEach(async(() => {
700+
fixture = TestBed.createComponent(DatepickerWithChangeAndInputEvents);
701+
fixture.detectChanges();
702+
703+
testComponent = fixture.componentInstance;
704+
inputEl = fixture.debugElement.query(By.css('input')).nativeElement;
705+
706+
spyOn(testComponent, 'onChange');
707+
spyOn(testComponent, 'onInput');
708+
spyOn(testComponent, 'onDateChange');
709+
spyOn(testComponent, 'onDateInput');
710+
}));
711+
712+
afterEach(async(() => {
713+
testComponent.datepicker.close();
714+
fixture.detectChanges();
715+
}));
716+
717+
it('should fire input and dateInput events when user types input', () => {
718+
expect(testComponent.onChange).not.toHaveBeenCalled();
719+
expect(testComponent.onDateChange).not.toHaveBeenCalled();
720+
expect(testComponent.onInput).not.toHaveBeenCalled();
721+
expect(testComponent.onDateInput).not.toHaveBeenCalled();
722+
723+
dispatchFakeEvent(inputEl, 'input');
724+
fixture.detectChanges();
725+
726+
expect(testComponent.onChange).not.toHaveBeenCalled();
727+
expect(testComponent.onDateChange).not.toHaveBeenCalled();
728+
expect(testComponent.onInput).toHaveBeenCalled();
729+
expect(testComponent.onDateInput).toHaveBeenCalled();
730+
});
731+
732+
it('should fire change and dateChange events when user commits typed input', () => {
733+
expect(testComponent.onChange).not.toHaveBeenCalled();
734+
expect(testComponent.onDateChange).not.toHaveBeenCalled();
735+
expect(testComponent.onInput).not.toHaveBeenCalled();
736+
expect(testComponent.onDateInput).not.toHaveBeenCalled();
737+
738+
dispatchFakeEvent(inputEl, 'change');
739+
fixture.detectChanges();
740+
741+
expect(testComponent.onChange).toHaveBeenCalled();
742+
expect(testComponent.onDateChange).toHaveBeenCalled();
743+
expect(testComponent.onInput).not.toHaveBeenCalled();
744+
expect(testComponent.onDateInput).not.toHaveBeenCalled();
745+
});
746+
747+
it('should fire dateChange and dateInput events when user selects calendar date', () => {
748+
expect(testComponent.onChange).not.toHaveBeenCalled();
749+
expect(testComponent.onDateChange).not.toHaveBeenCalled();
750+
expect(testComponent.onInput).not.toHaveBeenCalled();
751+
expect(testComponent.onDateInput).not.toHaveBeenCalled();
752+
753+
testComponent.datepicker.open();
754+
fixture.detectChanges();
755+
756+
expect(document.querySelector('md-dialog-container')).not.toBeNull();
757+
758+
let cells = document.querySelectorAll('.mat-calendar-body-cell');
759+
dispatchMouseEvent(cells[0], 'click');
760+
fixture.detectChanges();
761+
762+
expect(testComponent.onChange).not.toHaveBeenCalled();
763+
expect(testComponent.onDateChange).toHaveBeenCalled();
764+
expect(testComponent.onInput).not.toHaveBeenCalled();
765+
expect(testComponent.onDateInput).toHaveBeenCalled();
766+
});
767+
});
692768
});
693769

694770
describe('with missing DateAdapter and MD_DATE_FORMATS', () => {
@@ -924,3 +1000,23 @@ class DatepickerWithFilterAndValidation {
9241000
date: Date;
9251001
filter = (date: Date) => date.getDate() != 1;
9261002
}
1003+
1004+
1005+
@Component({
1006+
template: `
1007+
<input [mdDatepicker]="d" (change)="onChange()" (input)="onInput()"
1008+
(dateChange)="onDateChange()" (dateInput)="onDateInput()">
1009+
<md-datepicker #d [touchUi]="true"></md-datepicker>
1010+
`
1011+
})
1012+
class DatepickerWithChangeAndInputEvents {
1013+
@ViewChild('d') datepicker: MdDatepicker<Date>;
1014+
1015+
onChange() {}
1016+
1017+
onInput() {}
1018+
1019+
onDateChange() {}
1020+
1021+
onDateInput() {}
1022+
}

0 commit comments

Comments
 (0)