Skip to content

refactor: move common date input logic into base class #18213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions src/dev-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -171,35 +171,39 @@ <h2>Datepicker with custom header extending the default header</h2>
</p>

<h2>Range picker</h2>
<p>

<div class="demo-range-group">
<mat-form-field>
<mat-label>Enter a date range</mat-label>
<mat-date-range-input>
<input matStartDate placeholder="Start date">
<input matEndDate placeholder="End date">
<mat-date-range-input [formGroup]="range1">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
</mat-date-range-input>
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
</mat-form-field>
</p>
<div>{{range1.value | json}}</div>
</div>

<p>
<div class="demo-range-group">
<mat-form-field appearance="fill">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input>
<input matStartDate placeholder="Start date">
<input matEndDate placeholder="End date">
<mat-date-range-input [formGroup]="range2">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
</mat-date-range-input>
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
</mat-form-field>
</p>
<div>{{range2.value | json}}</div>
</div>

<p>
<div class="demo-range-group">
<mat-form-field appearance="outline">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input>
<input matStartDate placeholder="Start date">
<input matEndDate placeholder="End date">
<mat-date-range-input [formGroup]="range3">
<input matStartDate formControlName="start" placeholder="Start date"/>
<input matEndDate formControlName="end" placeholder="End date"/>
</mat-date-range-input>
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
</mat-form-field>
</p>
<div>{{range3.value | json}}</div>
</div>
3 changes: 3 additions & 0 deletions src/dev-app/datepicker/datepicker-demo.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ mat-calendar {
width: 300px;
}

.demo-range-group {
margin-bottom: 30px;
}
5 changes: 4 additions & 1 deletion src/dev-app/datepicker/datepicker-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
Optional,
ViewChild
} from '@angular/core';
import {FormControl} from '@angular/forms';
import {FormControl, FormGroup} from '@angular/forms';
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats, ThemePalette} from '@angular/material/core';
import {
MatCalendar,
Expand Down Expand Up @@ -47,6 +47,9 @@ export class DatepickerDemo {
color: ThemePalette;

dateCtrl = new FormControl();
range1 = new FormGroup({start: new FormControl(), end: new FormControl()});
range2 = new FormGroup({start: new FormControl(), end: new FormControl()});
range3 = new FormGroup({start: new FormControl(), end: new FormControl()});

dateFilter: (date: Date | null) => boolean =
(date: Date | null) => {
Expand Down
171 changes: 99 additions & 72 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,45 @@
* found in the LICENSE file at https://angular.io/license
*/

import {Directive, ElementRef, Optional, Self, InjectionToken, Inject} from '@angular/core';
import {
Directive,
ElementRef,
Optional,
InjectionToken,
Inject,
OnInit,
Injector,
InjectFlags,
DoCheck,
} from '@angular/core';
import {
NG_VALUE_ACCESSOR,
NG_VALIDATORS,
ControlValueAccessor,
Validator,
AbstractControl,
ValidationErrors,
NgForm,
FormGroupDirective,
NgControl,
ValidatorFn,
Validators,
} from '@angular/forms';
import {
CanUpdateErrorState,
CanDisable,
ErrorStateMatcher,
CanDisableCtor,
CanUpdateErrorStateCtor,
mixinErrorState,
mixinDisabled,
MAT_DATE_FORMATS,
DateAdapter,
MatDateFormats,
ErrorStateMatcher,
} from '@angular/material/core';
import {BooleanInput} from '@angular/cdk/coercion';
import {MatDatepickerInputBase} from './datepicker-input-base';

/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
export interface MatDateRangeInputParent {
id: string;
_ariaDescribedBy: string | null;
_ariaLabelledBy: string | null;
_handleChildValueChange: () => void;
_openDatepicker: () => void;
}

/**
Expand All @@ -44,72 +54,56 @@ export interface MatDateRangeInputParent {
export const MAT_DATE_RANGE_INPUT_PARENT =
new InjectionToken<MatDateRangeInputParent>('MAT_DATE_RANGE_INPUT_PARENT');

// Boilerplate for applying mixins to MatDateRangeInput.
/** @docs-private */
class MatDateRangeInputPartMixinBase {
constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
public _parentForm: NgForm,
public _parentFormGroup: FormGroupDirective,
/** @docs-private */
public ngControl: NgControl) {}
}
const _MatDateRangeInputMixinBase: CanDisableCtor &
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartMixinBase =
mixinErrorState(mixinDisabled(MatDateRangeInputPartMixinBase));

/**
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
*/
@Directive()
abstract class MatDateRangeInputPartBase<D> extends _MatDateRangeInputMixinBase implements
ControlValueAccessor, Validator, CanUpdateErrorState, CanDisable, CanUpdateErrorState {

private _onTouched = () => {};

constructor(
protected _elementRef: ElementRef<HTMLInputElement>,
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
defaultErrorStateMatcher: ErrorStateMatcher,
@Optional() parentForm: NgForm,
@Optional() parentFormGroup: FormGroupDirective,
@Optional() @Self() ngControl: NgControl) {
super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl);
}
class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements OnInit, DoCheck {
protected _validator: ValidatorFn | null;

/** @docs-private */
writeValue(_value: D | null): void {
// TODO(crisbeto): implement
}
ngControl: NgControl;

/** @docs-private */
registerOnChange(_fn: () => void): void {
// TODO(crisbeto): implement
}
updateErrorState: () => void;

/** @docs-private */
registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}

/** @docs-private */
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
constructor(
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
elementRef: ElementRef<HTMLInputElement>,
public _defaultErrorStateMatcher: ErrorStateMatcher,
private _injector: Injector,
@Optional() public _parentForm: NgForm,
@Optional() public _parentFormGroup: FormGroupDirective,
@Optional() dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
super(elementRef, dateAdapter, dateFormats);
}

/** @docs-private */
validate(_control: AbstractControl): ValidationErrors | null {
// TODO(crisbeto): implement
return null;
ngOnInit() {
// We need the date input to provide itself as a `ControlValueAccessor` and a `Validator`, while
// injecting its `NgControl` so that the error state is handled correctly. This introduces a
// circular dependency, because both `ControlValueAccessor` and `Validator` depend on the input
// itself. Usually we can work around it for the CVA, but there's no API to do it for the
// validator. We work around it here by injecting the `NgControl` in `ngOnInit`, after
// everything has been resolved.
const ngControl = this._injector.get(NgControl, null, InjectFlags.Self);

if (ngControl) {
this.ngControl = ngControl;
}
}

/** @docs-private */
registerOnValidatorChange(_fn: () => void): void {
// TODO(crisbeto): implement
ngDoCheck() {
if (this.ngControl) {
// We need to re-evaluate this on every change detection cycle, because there are some
// error triggers that we can't subscribe to (e.g. parent form submissions). This means
// that whatever logic is in here has to be super lean or we risk destroying the performance.
this.updateErrorState();
}
}

/** Gets whether the input is empty. */
isEmpty(): boolean {
// TODO(crisbeto): should look at the CVA value.
return this._elementRef.nativeElement.value.length === 0;
}

Expand All @@ -118,34 +112,55 @@ abstract class MatDateRangeInputPartBase<D> extends _MatDateRangeInputMixinBase
this._elementRef.nativeElement.focus();
}

/** Handles blur events on the input. */
_handleBlur(): void {
this._onTouched();
/** Handles `input` events on the input element. */
_onInput(value: string) {
super._onInput(value);
this._rangeInput._handleChildValueChange();
}

static ngAcceptInputType_disabled: BooleanInput;
/** Opens the datepicker associated with the input. */
protected _openPopup(): void {
this._rangeInput._openDatepicker();
}

protected _assignModelValue(_model: D | null): void {
// TODO(crisbeto): implement
}
}

const _MatDateRangeInputBase:
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartBase =
mixinErrorState(MatDateRangeInputPartBase);

/** Input for entering the start date in a `mat-date-range-input`. */
@Directive({
selector: 'input[matStartDate]',
inputs: ['disabled'],
host: {
'[id]': '_rangeInput.id',
'class': 'mat-date-range-input-inner',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
'(keydown)': '_onKeydown($event)',
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
'class': 'mat-date-range-input-inner',
'(blur)': '_onBlur()',
'type': 'text',
'(blur)': '_handleBlur()',
'(input)': '_rangeInput._handleChildValueChange()'

// TODO(crisbeto): to be added once the datepicker is implemented
// '[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
// '[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
// '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
// '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
},
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: MatStartDate, multi: true},
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
]
})
export class MatStartDate<D> extends MatDateRangeInputPartBase<D> {
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
// TODO(crisbeto): start-range-specific validators should go here.
protected _validator = Validators.compose([this._parseValidator]);

/** Gets the value that should be used when mirroring the input's size. */
getMirrorValue(): string {
const element = this._elementRef.nativeElement;
Expand All @@ -160,19 +175,31 @@ export class MatStartDate<D> extends MatDateRangeInputPartBase<D> {
/** Input for entering the end date in a `mat-date-range-input`. */
@Directive({
selector: 'input[matEndDate]',
inputs: ['disabled'],
host: {
'class': 'mat-date-range-input-inner',
'[disabled]': 'disabled',
'(input)': '_onInput($event.target.value)',
'(change)': '_onChange()',
'(keydown)': '_onKeydown($event)',
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
'(blur)': '_handleBlur',
'(blur)': '_onBlur()',
'type': 'text',

// TODO(crisbeto): to be added once the datepicker is implemented
// '[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
// '[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
// '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
// '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
},
providers: [
{provide: NG_VALUE_ACCESSOR, useExisting: MatEndDate, multi: true},
{provide: NG_VALIDATORS, useExisting: MatEndDate, multi: true}
]
})
export class MatEndDate<D> extends MatDateRangeInputPartBase<D> {
export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
// TODO(crisbeto): end-range-specific validators should go here.
protected _validator = Validators.compose([this._parseValidator]);

static ngAcceptInputType_disabled: BooleanInput;
}
6 changes: 6 additions & 0 deletions src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
*/
onContainerClick(): void {
if (!this.focused) {
// TODO(crisbeto): maybe this should go to end input if start has a value?
this._startInput.focus();
}
}
Expand Down Expand Up @@ -195,5 +196,10 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
this._changeDetectorRef.markForCheck();
}

/** Opens the datepicker associated with the input. */
_openDatepicker() {
// TODO(crisbeto): implement once the datepicker is in place.
}

static ngAcceptInputType_required: BooleanInput;
}
Loading