Skip to content

Commit

Permalink
fix(datepicker): better support for input and change events (#4826)
Browse files Browse the repository at this point in the history
  • Loading branch information
mmalerba authored and kara committed Jul 20, 2017
1 parent 1cba2dc commit 35eb294
Show file tree
Hide file tree
Showing 5 changed files with 152 additions and 2 deletions.
7 changes: 6 additions & 1 deletion src/demo-app/datepicker/datepicker-demo.html
Expand Up @@ -37,7 +37,9 @@ <h2>Result</h2>
[min]="minDate"
[max]="maxDate"
[mdDatepickerFilter]="filterOdd ? dateFilter : null"
placeholder="Pick a date">
placeholder="Pick a date"
(dateInput)="onDateInput($event)"
(dateChange)="onDateChange($event)">
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMin')">Too early!</md-error>
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerMax')">Too late!</md-error>
<md-error *ngIf="resultPickerModel.hasError('mdDatepickerFilter')">Date unavailable!</md-error>
Expand All @@ -50,6 +52,9 @@ <h2>Result</h2>
[startView]="yearView ? 'year' : 'month'">
</md-datepicker>
</p>
<p>Last input: {{lastDateInput}}</p>
<p>Last change: {{lastDateChange}}</p>
<br>
<p>
<input #resultPickerModel2
[mdDatepicker]="resultPicker2"
Expand Down
7 changes: 7 additions & 0 deletions src/demo-app/datepicker/datepicker-demo.ts
@@ -1,4 +1,5 @@
import {Component} from '@angular/core';
import {MdDatepickerInputEvent} from '@angular/material';


@Component({
Expand All @@ -17,5 +18,11 @@ export class DatepickerDemo {
maxDate: Date;
startAt: Date;
date: Date;
lastDateInput: Date | null;
lastDateChange: Date | null;

dateFilter = (date: Date) => date.getMonth() % 2 == 1 && date.getDate() % 2 == 0;

onDateInput = (e: MdDatepickerInputEvent<Date>) => this.lastDateInput = e.value;
onDateChange = (e: MdDatepickerInputEvent<Date>) => this.lastDateChange = e.value;
}
33 changes: 32 additions & 1 deletion src/lib/datepicker/datepicker-input.ts
Expand Up @@ -16,6 +16,7 @@ import {
Input,
OnDestroy,
Optional,
Output,
Renderer2
} from '@angular/core';
import {MdDatepicker} from './datepicker';
Expand Down Expand Up @@ -52,6 +53,21 @@ export const MD_DATEPICKER_VALIDATORS: any = {
};


/**
* An event used for datepicker input and change events. We don't always have access to a native
* input or change event because the event may have been triggered by the user clicking on the
* calendar popup. For consistency, we always use MdDatepickerInputEvent instead.
*/
export class MdDatepickerInputEvent<D> {
/** The new value for the target datepicker input. */
value: D | null;

constructor(public target: MdDatepickerInput<D>, public targetElement: HTMLElement) {
this.value = this.target.value;
}
}


/** Directive used to connect an input to a MdDatepicker. */
@Directive({
selector: 'input[mdDatepicker], input[matDatepicker]',
Expand All @@ -63,9 +79,11 @@ export const MD_DATEPICKER_VALIDATORS: any = {
'[attr.max]': 'max ? _dateAdapter.getISODateString(max) : null',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
'(blur)': '_onTouched()',
'(keydown)': '_onKeydown($event)',
}
},
exportAs: 'mdDatepickerInput',
})
export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAccessor, OnDestroy,
Validator {
Expand Down Expand Up @@ -133,6 +151,12 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
}
private _disabled: boolean;

/** Emits when a `change` event is fired on this `<input>`. */
@Output() dateChange = new EventEmitter<MdDatepickerInputEvent<D>>();

/** Emits when an `input` event is fired on this `<input>`. */
@Output() dateInput = new EventEmitter<MdDatepickerInputEvent<D>>();

/** Emits when the value changes (either due to user input or programmatic change). */
_valueChange = new EventEmitter<D|null>();

Expand Down Expand Up @@ -188,6 +212,8 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
this._datepicker.selectedChanged.subscribe((selected: D) => {
this.value = selected;
this._cvaOnChange(selected);
this.dateInput.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
this.dateChange.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
});
}
}
Expand Down Expand Up @@ -245,5 +271,10 @@ export class MdDatepickerInput<D> implements AfterContentInit, ControlValueAcces
let date = this._dateAdapter.parse(value, this._dateFormats.parse.dateInput);
this._cvaOnChange(date);
this._valueChange.emit(date);
this.dateInput.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
}

_onChange() {
this.dateChange.emit(new MdDatepickerInputEvent(this, this._elementRef.nativeElement));
}
}
11 changes: 11 additions & 0 deletions src/lib/datepicker/datepicker.md
Expand Up @@ -82,6 +82,17 @@ Each validation property has a different error that can be checked:
* A value that violates the `min` property will have a `mdDatepickerMin` error.
* A value that violates the `max` property will have a `mdDatepickerMax` error.
* A value that violates the `mdDatepickerFilter` property will have a `mdDatepickerFilter` error.

### Input and change events
The input's native `input` and `change` events will only trigger due to user interaction with the
input element; they will not fire when the user selects a date from the calendar popup. Because of
this limitation, the datepicker input also has support for `dateInput` and `dateChange` events.
These trigger when the user interacts with either the input or the popup.

```html
<input [mdDatepicker]="d" (dateInput)="onInput($event)" (dateChange)="onChange($event)">
<md-datepicker #d></md-datepicker>
```

### Touch UI mode
The datepicker normally opens as a popup under the input. However this is not ideal for touch
Expand Down
96 changes: 96 additions & 0 deletions src/lib/datepicker/datepicker.spec.ts
Expand Up @@ -29,6 +29,7 @@ describe('MdDatepicker', () => {
ReactiveFormsModule,
],
declarations: [
DatepickerWithChangeAndInputEvents,
DatepickerWithFilterAndValidation,
DatepickerWithFormControl,
DatepickerWithMinAndMaxValidation,
Expand Down Expand Up @@ -689,6 +690,81 @@ describe('MdDatepicker', () => {
expect(cells[1].classList).not.toContain('mat-calendar-body-disabled');
});
});

describe('datepicker with change and input events', () => {
let fixture: ComponentFixture<DatepickerWithChangeAndInputEvents>;
let testComponent: DatepickerWithChangeAndInputEvents;
let inputEl: HTMLInputElement;

beforeEach(async(() => {
fixture = TestBed.createComponent(DatepickerWithChangeAndInputEvents);
fixture.detectChanges();

testComponent = fixture.componentInstance;
inputEl = fixture.debugElement.query(By.css('input')).nativeElement;

spyOn(testComponent, 'onChange');
spyOn(testComponent, 'onInput');
spyOn(testComponent, 'onDateChange');
spyOn(testComponent, 'onDateInput');
}));

afterEach(async(() => {
testComponent.datepicker.close();
fixture.detectChanges();
}));

it('should fire input and dateInput events when user types input', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
expect(testComponent.onDateChange).not.toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).not.toHaveBeenCalled();

dispatchFakeEvent(inputEl, 'input');
fixture.detectChanges();

expect(testComponent.onChange).not.toHaveBeenCalled();
expect(testComponent.onDateChange).not.toHaveBeenCalled();
expect(testComponent.onInput).toHaveBeenCalled();
expect(testComponent.onDateInput).toHaveBeenCalled();
});

it('should fire change and dateChange events when user commits typed input', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
expect(testComponent.onDateChange).not.toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).not.toHaveBeenCalled();

dispatchFakeEvent(inputEl, 'change');
fixture.detectChanges();

expect(testComponent.onChange).toHaveBeenCalled();
expect(testComponent.onDateChange).toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).not.toHaveBeenCalled();
});

it('should fire dateChange and dateInput events when user selects calendar date', () => {
expect(testComponent.onChange).not.toHaveBeenCalled();
expect(testComponent.onDateChange).not.toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).not.toHaveBeenCalled();

testComponent.datepicker.open();
fixture.detectChanges();

expect(document.querySelector('md-dialog-container')).not.toBeNull();

let cells = document.querySelectorAll('.mat-calendar-body-cell');
dispatchMouseEvent(cells[0], 'click');
fixture.detectChanges();

expect(testComponent.onChange).not.toHaveBeenCalled();
expect(testComponent.onDateChange).toHaveBeenCalled();
expect(testComponent.onInput).not.toHaveBeenCalled();
expect(testComponent.onDateInput).toHaveBeenCalled();
});
});
});

describe('with missing DateAdapter and MD_DATE_FORMATS', () => {
Expand Down Expand Up @@ -924,3 +1000,23 @@ class DatepickerWithFilterAndValidation {
date: Date;
filter = (date: Date) => date.getDate() != 1;
}


@Component({
template: `
<input [mdDatepicker]="d" (change)="onChange()" (input)="onInput()"
(dateChange)="onDateChange()" (dateInput)="onDateInput()">
<md-datepicker #d [touchUi]="true"></md-datepicker>
`
})
class DatepickerWithChangeAndInputEvents {
@ViewChild('d') datepicker: MdDatepicker<Date>;

onChange() {}

onInput() {}

onDateChange() {}

onDateInput() {}
}

0 comments on commit 35eb294

Please sign in to comment.