Skip to content

Commit d02a30d

Browse files
committed
refactor(datepicker): set up date range picker classes (#18292)
Moves some things around to set up the date range picker. Includes: * Moves all of the datepicker logic into a base class so that it can be reused between the single and range selection. * Makes the input being passed into a datepicker generic so that we can strongly type `MatDatepicker` to `MatDatepickerInput` and `MatDateRangePicker` to `MatDateRangeInput`. * Sets up min, max and date filter validation for the date range picker. * Hooks up the date range picker to the date range input. * Makes it so that a generic datepicker can be passed to a datepicker toggle. * Adds a `DateFilterFn` type so we don't have to repeat the signature for the filter function everywhere. Note that while picking a date range somewhat works now, it's still a bit broken. The primary goal of these changes was to have all of components in place and wired up so that we can start working on the UI in follow-up PRs.
1 parent c63f72c commit d02a30d

File tree

12 files changed

+867
-640
lines changed

12 files changed

+867
-640
lines changed

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

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -175,35 +175,38 @@ <h2>Range picker</h2>
175175
<div class="demo-range-group">
176176
<mat-form-field>
177177
<mat-label>Enter a date range</mat-label>
178-
<mat-date-range-input [formGroup]="range1">
178+
<mat-date-range-input [formGroup]="range1" [rangePicker]="range1Picker">
179179
<input matStartDate formControlName="start" placeholder="Start date"/>
180180
<input matEndDate formControlName="end" placeholder="End date"/>
181181
</mat-date-range-input>
182-
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
182+
<mat-datepicker-toggle [for]="range1Picker" matSuffix></mat-datepicker-toggle>
183+
<mat-date-range-picker #range1Picker></mat-date-range-picker>
183184
</mat-form-field>
184185
<div>{{range1.value | json}}</div>
185186
</div>
186187

187188
<div class="demo-range-group">
188189
<mat-form-field appearance="fill">
189190
<mat-label>Enter a date range</mat-label>
190-
<mat-date-range-input [formGroup]="range2">
191+
<mat-date-range-input [formGroup]="range2" [rangePicker]="range2Picker">
191192
<input matStartDate formControlName="start" placeholder="Start date"/>
192193
<input matEndDate formControlName="end" placeholder="End date"/>
193194
</mat-date-range-input>
194-
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
195+
<mat-datepicker-toggle [for]="range2Picker" matSuffix></mat-datepicker-toggle>
196+
<mat-date-range-picker #range2Picker></mat-date-range-picker>
195197
</mat-form-field>
196198
<div>{{range2.value | json}}</div>
197199
</div>
198200

199201
<div class="demo-range-group">
200202
<mat-form-field appearance="outline">
201203
<mat-label>Enter a date range</mat-label>
202-
<mat-date-range-input [formGroup]="range3">
204+
<mat-date-range-input [formGroup]="range3" [rangePicker]="range3Picker">
203205
<input matStartDate formControlName="start" placeholder="Start date"/>
204206
<input matEndDate formControlName="end" placeholder="End date"/>
205207
</mat-date-range-input>
206-
<mat-datepicker-toggle matSuffix></mat-datepicker-toggle>
208+
<mat-datepicker-toggle [for]="range3Picker" matSuffix></mat-datepicker-toggle>
209+
<mat-date-range-picker #range3Picker></mat-date-range-picker>
207210
</mat-form-field>
208211
<div>{{range3.value | json}}</div>
209212
</div>

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,14 +35,16 @@ import {
3535
MatDateFormats,
3636
ErrorStateMatcher,
3737
DateRange,
38-
MatDateSelectionModel,
3938
} from '@angular/material/core';
4039
import {BooleanInput} from '@angular/cdk/coercion';
41-
import {MatDatepickerInputBase} from './datepicker-input-base';
40+
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';
4241

4342
/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
44-
export interface MatDateRangeInputParent {
43+
export interface MatDateRangeInputParent<D> {
4544
id: string;
45+
min: D | null;
46+
max: D | null;
47+
dateFilter: DateFilterFn<D>;
4648
_ariaDescribedBy: string | null;
4749
_ariaLabelledBy: string | null;
4850
_handleChildValueChange: () => void;
@@ -54,14 +56,14 @@ export interface MatDateRangeInputParent {
5456
* to the parts without circular dependencies.
5557
*/
5658
export const MAT_DATE_RANGE_INPUT_PARENT =
57-
new InjectionToken<MatDateRangeInputParent>('MAT_DATE_RANGE_INPUT_PARENT');
59+
new InjectionToken<MatDateRangeInputParent<unknown>>('MAT_DATE_RANGE_INPUT_PARENT');
5860

5961
/**
6062
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
6163
*/
6264
@Directive()
6365
abstract class MatDateRangeInputPartBase<D>
64-
extends MatDatepickerInputBase<DateRange<D>, D> implements OnInit, DoCheck {
66+
extends MatDatepickerInputBase<DateRange<D>> implements OnInit, DoCheck {
6567

6668
/** @docs-private */
6769
ngControl: NgControl;
@@ -74,20 +76,15 @@ abstract class MatDateRangeInputPartBase<D>
7476
protected abstract _getValueFromModel(modelValue: DateRange<D>): D | null;
7577

7678
constructor(
77-
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
79+
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent<D>,
7880
elementRef: ElementRef<HTMLInputElement>,
7981
public _defaultErrorStateMatcher: ErrorStateMatcher,
8082
private _injector: Injector,
8183
@Optional() public _parentForm: NgForm,
8284
@Optional() public _parentFormGroup: FormGroupDirective,
8385
@Optional() dateAdapter: DateAdapter<D>,
84-
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
85-
86-
// TODO(crisbeto): this will be provided by the datepicker eventually.
87-
// We provide it here for the moment so we have something to test against.
88-
model: MatDateSelectionModel<DateRange<D>, D>) {
86+
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
8987
super(elementRef, dateAdapter, dateFormats);
90-
super._registerModel(model);
9188
}
9289

9390
ngOnInit() {
@@ -133,6 +130,21 @@ abstract class MatDateRangeInputPartBase<D>
133130
protected _openPopup(): void {
134131
this._rangeInput._openDatepicker();
135132
}
133+
134+
/** Gets the minimum date from the range input. */
135+
protected _getMinDate() {
136+
return this._rangeInput.min;
137+
}
138+
139+
/** Gets the maximum date from the range input. */
140+
protected _getMaxDate() {
141+
return this._rangeInput.max;
142+
}
143+
144+
/** Gets the date filter function from the range input. */
145+
protected _getDateFilter() {
146+
return this._rangeInput.dateFilter;
147+
}
136148
}
137149

138150
const _MatDateRangeInputBase:
@@ -165,10 +177,9 @@ const _MatDateRangeInputBase:
165177
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
166178
]
167179
})
168-
export class MatStartDate<D> extends _MatDateRangeInputBase<D>
169-
implements CanUpdateErrorState {
180+
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
170181
// TODO(crisbeto): start-range-specific validators should go here.
171-
protected _validator = Validators.compose([this._parseValidator]);
182+
protected _validator = Validators.compose(super._getValidators());
172183

173184
protected _getValueFromModel(modelValue: DateRange<D>) {
174185
return modelValue.start;
@@ -180,6 +191,13 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D>
180191
}
181192
}
182193

194+
protected _formatValue(value: D | null) {
195+
super._formatValue(value);
196+
197+
// Any time the input value is reformatted we need to tell the parent.
198+
this._rangeInput._handleChildValueChange();
199+
}
200+
183201
/** Gets the value that should be used when mirroring the input's size. */
184202
getMirrorValue(): string {
185203
const element = this._elementRef.nativeElement;
@@ -218,7 +236,7 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D>
218236
})
219237
export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
220238
// TODO(crisbeto): end-range-specific validators should go here.
221-
protected _validator = Validators.compose([this._parseValidator]);
239+
protected _validator = Validators.compose(super._getValidators());
222240

223241
protected _getValueFromModel(modelValue: DateRange<D>) {
224242
return modelValue.end;

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

Lines changed: 129 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,28 @@ import {
1717
AfterContentInit,
1818
ChangeDetectorRef,
1919
Self,
20+
ElementRef,
2021
} from '@angular/core';
2122
import {MatFormFieldControl, MatFormField} from '@angular/material/form-field';
22-
import {DateRange, MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER} from '@angular/material/core';
23+
import {
24+
DateRange,
25+
ThemePalette,
26+
DateAdapter,
27+
MatDateSelectionModel,
28+
} from '@angular/material/core';
2329
import {NgControl, ControlContainer} from '@angular/forms';
24-
import {Subject} from 'rxjs';
30+
import {Subject, merge} from 'rxjs';
2531
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
2632
import {
2733
MatStartDate,
2834
MatEndDate,
2935
MatDateRangeInputParent,
3036
MAT_DATE_RANGE_INPUT_PARENT,
3137
} from './date-range-input-parts';
38+
import {MatDatepickerControl} from './datepicker-base';
39+
import {createMissingDateImplError} from './datepicker-errors';
40+
import {DateFilterFn} from './datepicker-input-base';
41+
import {MatDateRangePicker} from './date-range-picker';
3242

3343
let nextUniqueId = 0;
3444

@@ -49,16 +59,14 @@ let nextUniqueId = 0;
4959
providers: [
5060
{provide: MatFormFieldControl, useExisting: MatDateRangeInput},
5161
{provide: MAT_DATE_RANGE_INPUT_PARENT, useExisting: MatDateRangeInput},
52-
53-
// TODO(crisbeto): this will be provided by the datepicker eventually.
54-
// We provide it here for the moment so we have something to test against.
55-
MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
5662
]
5763
})
5864
export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
59-
MatDateRangeInputParent, AfterContentInit, OnDestroy {
65+
MatDatepickerControl<D>, MatDateRangeInputParent<D>, AfterContentInit, OnDestroy {
6066
/** Current value of the range input. */
61-
value: DateRange<D> | null = null;
67+
get value() {
68+
return this._model ? this._model.selection : null;
69+
}
6270

6371
/** Emits when the input's state has changed. */
6472
stateChanges = new Subject<void>();
@@ -79,11 +87,23 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
7987

8088
/**
8189
* Implemented as a part of `MatFormFieldControl`, but not used.
82-
* Use `startPlaceholder` and `endPlaceholder` instead.
90+
* Set the placeholder attribute on `matStartDate` and `matEndDate`.
8391
* @docs-private
8492
*/
8593
placeholder: string;
8694

95+
/** The range picker that this input is associated with. */
96+
@Input()
97+
get rangePicker() { return this._rangePicker; }
98+
set rangePicker(rangePicker: MatDateRangePicker<D>) {
99+
if (rangePicker) {
100+
this._model = rangePicker._registerInput(this);
101+
this._rangePicker = rangePicker;
102+
this._registerModel(this._model!);
103+
}
104+
}
105+
private _rangePicker: MatDateRangePicker<D>;
106+
87107
/** Whether the input is required. */
88108
@Input()
89109
get required(): boolean { return !!this._required; }
@@ -92,6 +112,33 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
92112
}
93113
private _required: boolean;
94114

115+
/** Function that can be used to filter out dates within the date range picker. */
116+
@Input()
117+
get dateFilter() { return this._dateFilter; }
118+
set dateFilter(value: DateFilterFn<D>) {
119+
this._dateFilter = value;
120+
this._revalidate();
121+
}
122+
private _dateFilter: DateFilterFn<D>;
123+
124+
/** The minimum valid date. */
125+
@Input()
126+
get min(): D | null { return this._min; }
127+
set min(value: D | null) {
128+
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
129+
this._revalidate();
130+
}
131+
private _min: D | null;
132+
133+
/** The maximum valid date. */
134+
@Input()
135+
get max(): D | null { return this._max; }
136+
set max(value: D | null) {
137+
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
138+
this._revalidate();
139+
}
140+
private _max: D | null;
141+
95142
/** Whether the input is disabled. */
96143
get disabled(): boolean {
97144
if (this._startInput && this._endInput) {
@@ -123,11 +170,8 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
123170
/** Value for the `aria-labelledby` attribute of the inputs. */
124171
_ariaLabelledBy: string | null = null;
125172

126-
/** Placeholder for the start input. */
127-
@Input() startPlaceholder: string;
128-
129-
/** Placeholder for the end input. */
130-
@Input() endPlaceholder: string;
173+
/** Date selection model currently registered with the input. */
174+
private _model: MatDateSelectionModel<DateRange<D>> | undefined;
131175

132176
/** Separator text to be shown between the inputs. */
133177
@Input() separator = '–';
@@ -142,14 +186,23 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
142186
*/
143187
ngControl: NgControl | null;
144188

189+
/** Emits when the input's disabled state changes. */
190+
_disabledChange = new Subject<boolean>();
191+
145192
constructor(
146193
private _changeDetectorRef: ChangeDetectorRef,
194+
private _elementRef: ElementRef<HTMLElement>,
147195
@Optional() @Self() control: ControlContainer,
148-
@Optional() formField?: MatFormField) {
196+
@Optional() private _dateAdapter: DateAdapter<D>,
197+
@Optional() private _formField?: MatFormField) {
198+
199+
if (!_dateAdapter) {
200+
throw createMissingDateImplError('DateAdapter');
201+
}
149202

150203
// TODO(crisbeto): remove `as any` after #18206 lands.
151204
this.ngControl = control as any;
152-
this._ariaLabelledBy = formField ? formField._labelId : null;
205+
this._ariaLabelledBy = _formField ? _formField._labelId : null;
153206
}
154207

155208
/**
@@ -179,10 +232,36 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
179232
if (!this._endInput) {
180233
throw Error('mat-date-range-input must contain a matEndDate input');
181234
}
235+
236+
if (this._model) {
237+
this._registerModel(this._model);
238+
}
239+
240+
// We don't need to unsubscribe from this, because we
241+
// know that the input streams will be completed on destroy.
242+
merge(this._startInput._disabledChange, this._endInput._disabledChange).subscribe(() => {
243+
this._disabledChange.next(this.disabled);
244+
});
182245
}
183246

184247
ngOnDestroy() {
185248
this.stateChanges.complete();
249+
this._disabledChange.unsubscribe();
250+
}
251+
252+
/** Gets the date at which the calendar should start. */
253+
getStartValue(): D | null {
254+
return this.value ? this.value.start : null;
255+
}
256+
257+
/** Gets the input's theme palette. */
258+
getThemePalette(): ThemePalette {
259+
return this._formField ? this._formField.color : undefined;
260+
}
261+
262+
/** Gets the element to which the calendar overlay should be attached. */
263+
getConnectedOverlayOrigin(): ElementRef {
264+
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
186265
}
187266

188267
/** Gets the value that is used to mirror the state input. */
@@ -200,9 +279,41 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
200279
this._changeDetectorRef.markForCheck();
201280
}
202281

203-
/** Opens the datepicker associated with the input. */
282+
/** Opens the date range picker associated with the input. */
204283
_openDatepicker() {
205-
// TODO(crisbeto): implement once the datepicker is in place.
284+
if (this._rangePicker) {
285+
this._rangePicker.open();
286+
}
287+
}
288+
289+
/**
290+
* @param obj The object to check.
291+
* @returns The given object if it is both a date instance and valid, otherwise null.
292+
*/
293+
private _getValidDateOrNull(obj: any): D | null {
294+
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
295+
}
296+
297+
/** Re-runs the validators on the start/end inputs. */
298+
private _revalidate() {
299+
if (this._startInput) {
300+
this._startInput._validatorOnChange();
301+
}
302+
303+
if (this._endInput) {
304+
this._endInput._validatorOnChange();
305+
}
306+
}
307+
308+
/** Registers the current date selection model with the start/end inputs. */
309+
private _registerModel(model: MatDateSelectionModel<DateRange<D>>) {
310+
if (this._startInput) {
311+
this._startInput._registerModel(model);
312+
}
313+
314+
if (this._endInput) {
315+
this._endInput._registerModel(model);
316+
}
206317
}
207318

208319
static ngAcceptInputType_required: BooleanInput;

0 commit comments

Comments
 (0)