Skip to content

Commit d8bea76

Browse files
committed
refactor: move common date input logic into base class (#18213)
Moves the common logic for a date input into a base class and implements it for the range inputs.
1 parent fba8c21 commit d8bea76

File tree

9 files changed

+479
-379
lines changed

9 files changed

+479
-379
lines changed

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

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -171,35 +171,39 @@ <h2>Datepicker with custom header extending the default header</h2>
171171
</p>
172172

173173
<h2>Range picker</h2>
174-
<p>
174+
175+
<div class="demo-range-group">
175176
<mat-form-field>
176177
<mat-label>Enter a date range</mat-label>
177-
<mat-date-range-input>
178-
<input matStartDate placeholder="Start date">
179-
<input matEndDate placeholder="End date">
178+
<mat-date-range-input [formGroup]="range1">
179+
<input matStartDate formControlName="start" placeholder="Start date"/>
180+
<input matEndDate formControlName="end" placeholder="End date"/>
180181
</mat-date-range-input>
181182
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
182183
</mat-form-field>
183-
</p>
184+
<div>{{range1.value | json}}</div>
185+
</div>
184186

185-
<p>
187+
<div class="demo-range-group">
186188
<mat-form-field appearance="fill">
187189
<mat-label>Enter a date range</mat-label>
188-
<mat-date-range-input>
189-
<input matStartDate placeholder="Start date">
190-
<input matEndDate placeholder="End date">
190+
<mat-date-range-input [formGroup]="range2">
191+
<input matStartDate formControlName="start" placeholder="Start date"/>
192+
<input matEndDate formControlName="end" placeholder="End date"/>
191193
</mat-date-range-input>
192194
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
193195
</mat-form-field>
194-
</p>
196+
<div>{{range2.value | json}}</div>
197+
</div>
195198

196-
<p>
199+
<div class="demo-range-group">
197200
<mat-form-field appearance="outline">
198201
<mat-label>Enter a date range</mat-label>
199-
<mat-date-range-input>
200-
<input matStartDate placeholder="Start date">
201-
<input matEndDate placeholder="End date">
202+
<mat-date-range-input [formGroup]="range3">
203+
<input matStartDate formControlName="start" placeholder="Start date"/>
204+
<input matEndDate formControlName="end" placeholder="End date"/>
202205
</mat-date-range-input>
203206
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
204207
</mat-form-field>
205-
</p>
208+
<div>{{range3.value | json}}</div>
209+
</div>

src/dev-app/datepicker/datepicker-demo.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ mat-calendar {
22
width: 300px;
33
}
44

5+
.demo-range-group {
6+
margin-bottom: 30px;
7+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
Optional,
1616
ViewChild
1717
} from '@angular/core';
18-
import {FormControl} from '@angular/forms';
18+
import {FormControl, FormGroup} from '@angular/forms';
1919
import {DateAdapter, MAT_DATE_FORMATS, MatDateFormats, ThemePalette} from '@angular/material/core';
2020
import {
2121
MatCalendar,
@@ -47,6 +47,9 @@ export class DatepickerDemo {
4747
color: ThemePalette;
4848

4949
dateCtrl = new FormControl();
50+
range1 = new FormGroup({start: new FormControl(), end: new FormControl()});
51+
range2 = new FormGroup({start: new FormControl(), end: new FormControl()});
52+
range3 = new FormGroup({start: new FormControl(), end: new FormControl()});
5053

5154
dateFilter: (date: Date | null) => boolean =
5255
(date: Date | null) => {

src/material/datepicker/date-range-input-parts.ts

Lines changed: 99 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -6,35 +6,45 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Directive, ElementRef, Optional, Self, InjectionToken, Inject} from '@angular/core';
9+
import {
10+
Directive,
11+
ElementRef,
12+
Optional,
13+
InjectionToken,
14+
Inject,
15+
OnInit,
16+
Injector,
17+
InjectFlags,
18+
DoCheck,
19+
} from '@angular/core';
1020
import {
1121
NG_VALUE_ACCESSOR,
1222
NG_VALIDATORS,
13-
ControlValueAccessor,
14-
Validator,
15-
AbstractControl,
16-
ValidationErrors,
1723
NgForm,
1824
FormGroupDirective,
1925
NgControl,
26+
ValidatorFn,
27+
Validators,
2028
} from '@angular/forms';
2129
import {
2230
CanUpdateErrorState,
23-
CanDisable,
24-
ErrorStateMatcher,
25-
CanDisableCtor,
2631
CanUpdateErrorStateCtor,
2732
mixinErrorState,
28-
mixinDisabled,
33+
MAT_DATE_FORMATS,
34+
DateAdapter,
35+
MatDateFormats,
36+
ErrorStateMatcher,
2937
} from '@angular/material/core';
3038
import {BooleanInput} from '@angular/cdk/coercion';
39+
import {MatDatepickerInputBase} from './datepicker-input-base';
3140

32-
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
41+
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
3342
export interface MatDateRangeInputParent {
3443
id: string;
3544
_ariaDescribedBy: string | null;
3645
_ariaLabelledBy: string | null;
3746
_handleChildValueChange: () => void;
47+
_openDatepicker: () => void;
3848
}
3949

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

47-
// Boilerplate for applying mixins to MatDateRangeInput.
48-
/** @docs-private */
49-
class MatDateRangeInputPartMixinBase {
50-
constructor(public _defaultErrorStateMatcher: ErrorStateMatcher,
51-
public _parentForm: NgForm,
52-
public _parentFormGroup: FormGroupDirective,
53-
/** @docs-private */
54-
public ngControl: NgControl) {}
55-
}
56-
const _MatDateRangeInputMixinBase: CanDisableCtor &
57-
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartMixinBase =
58-
mixinErrorState(mixinDisabled(MatDateRangeInputPartMixinBase));
59-
6057
/**
6158
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
6259
*/
6360
@Directive()
64-
abstract class MatDateRangeInputPartBase<D> extends _MatDateRangeInputMixinBase implements
65-
ControlValueAccessor, Validator, CanUpdateErrorState, CanDisable, CanUpdateErrorState {
66-
67-
private _onTouched = () => {};
68-
69-
constructor(
70-
protected _elementRef: ElementRef<HTMLInputElement>,
71-
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
72-
defaultErrorStateMatcher: ErrorStateMatcher,
73-
@Optional() parentForm: NgForm,
74-
@Optional() parentFormGroup: FormGroupDirective,
75-
@Optional() @Self() ngControl: NgControl) {
76-
super(defaultErrorStateMatcher, parentForm, parentFormGroup, ngControl);
77-
}
61+
class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements OnInit, DoCheck {
62+
protected _validator: ValidatorFn | null;
7863

7964
/** @docs-private */
80-
writeValue(_value: D | null): void {
81-
// TODO(crisbeto): implement
82-
}
65+
ngControl: NgControl;
8366

8467
/** @docs-private */
85-
registerOnChange(_fn: () => void): void {
86-
// TODO(crisbeto): implement
87-
}
68+
updateErrorState: () => void;
8869

89-
/** @docs-private */
90-
registerOnTouched(fn: () => void): void {
91-
this._onTouched = fn;
92-
}
93-
94-
/** @docs-private */
95-
setDisabledState(isDisabled: boolean): void {
96-
this.disabled = isDisabled;
70+
constructor(
71+
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
72+
elementRef: ElementRef<HTMLInputElement>,
73+
public _defaultErrorStateMatcher: ErrorStateMatcher,
74+
private _injector: Injector,
75+
@Optional() public _parentForm: NgForm,
76+
@Optional() public _parentFormGroup: FormGroupDirective,
77+
@Optional() dateAdapter: DateAdapter<D>,
78+
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
79+
super(elementRef, dateAdapter, dateFormats);
9780
}
9881

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

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

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

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

121-
/** Handles blur events on the input. */
122-
_handleBlur(): void {
123-
this._onTouched();
115+
/** Handles `input` events on the input element. */
116+
_onInput(value: string) {
117+
super._onInput(value);
118+
this._rangeInput._handleChildValueChange();
124119
}
125120

126-
static ngAcceptInputType_disabled: BooleanInput;
121+
/** Opens the datepicker associated with the input. */
122+
protected _openPopup(): void {
123+
this._rangeInput._openDatepicker();
124+
}
125+
126+
protected _assignModelValue(_model: D | null): void {
127+
// TODO(crisbeto): implement
128+
}
127129
}
128130

131+
const _MatDateRangeInputBase:
132+
CanUpdateErrorStateCtor & typeof MatDateRangeInputPartBase =
133+
mixinErrorState(MatDateRangeInputPartBase);
129134

130135
/** Input for entering the start date in a `mat-date-range-input`. */
131136
@Directive({
132137
selector: 'input[matStartDate]',
133-
inputs: ['disabled'],
134138
host: {
135-
'[id]': '_rangeInput.id',
139+
'class': 'mat-date-range-input-inner',
140+
'[disabled]': 'disabled',
141+
'(input)': '_onInput($event.target.value)',
142+
'(change)': '_onChange()',
143+
'(keydown)': '_onKeydown($event)',
136144
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
137145
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
138-
'class': 'mat-date-range-input-inner',
146+
'(blur)': '_onBlur()',
139147
'type': 'text',
140-
'(blur)': '_handleBlur()',
141-
'(input)': '_rangeInput._handleChildValueChange()'
148+
149+
// TODO(crisbeto): to be added once the datepicker is implemented
150+
// '[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
151+
// '[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
152+
// '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
153+
// '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
142154
},
143155
providers: [
144156
{provide: NG_VALUE_ACCESSOR, useExisting: MatStartDate, multi: true},
145157
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
146158
]
147159
})
148-
export class MatStartDate<D> extends MatDateRangeInputPartBase<D> {
160+
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
161+
// TODO(crisbeto): start-range-specific validators should go here.
162+
protected _validator = Validators.compose([this._parseValidator]);
163+
149164
/** Gets the value that should be used when mirroring the input's size. */
150165
getMirrorValue(): string {
151166
const element = this._elementRef.nativeElement;
@@ -160,19 +175,31 @@ export class MatStartDate<D> extends MatDateRangeInputPartBase<D> {
160175
/** Input for entering the end date in a `mat-date-range-input`. */
161176
@Directive({
162177
selector: 'input[matEndDate]',
163-
inputs: ['disabled'],
164178
host: {
165179
'class': 'mat-date-range-input-inner',
180+
'[disabled]': 'disabled',
181+
'(input)': '_onInput($event.target.value)',
182+
'(change)': '_onChange()',
183+
'(keydown)': '_onKeydown($event)',
166184
'[attr.aria-labelledby]': '_rangeInput._ariaLabelledBy',
167185
'[attr.aria-describedby]': '_rangeInput._ariaDescribedBy',
168-
'(blur)': '_handleBlur',
186+
'(blur)': '_onBlur()',
169187
'type': 'text',
188+
189+
// TODO(crisbeto): to be added once the datepicker is implemented
190+
// '[attr.aria-haspopup]': '_datepicker ? "dialog" : null',
191+
// '[attr.aria-owns]': '(_datepicker?.opened && _datepicker.id) || null',
192+
// '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null',
193+
// '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null',
170194
},
171195
providers: [
172196
{provide: NG_VALUE_ACCESSOR, useExisting: MatEndDate, multi: true},
173197
{provide: NG_VALIDATORS, useExisting: MatEndDate, multi: true}
174198
]
175199
})
176-
export class MatEndDate<D> extends MatDateRangeInputPartBase<D> {
200+
export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
201+
// TODO(crisbeto): end-range-specific validators should go here.
202+
protected _validator = Validators.compose([this._parseValidator]);
203+
177204
static ngAcceptInputType_disabled: BooleanInput;
178205
}

src/material/datepicker/date-range-input.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
162162
*/
163163
onContainerClick(): void {
164164
if (!this.focused) {
165+
// TODO(crisbeto): maybe this should go to end input if start has a value?
165166
this._startInput.focus();
166167
}
167168
}
@@ -195,5 +196,10 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
195196
this._changeDetectorRef.markForCheck();
196197
}
197198

199+
/** Opens the datepicker associated with the input. */
200+
_openDatepicker() {
201+
// TODO(crisbeto): implement once the datepicker is in place.
202+
}
203+
198204
static ngAcceptInputType_required: BooleanInput;
199205
}

0 commit comments

Comments
 (0)