Skip to content

Commit fd99256

Browse files
authored
refactor(datepicker): implement range selection model (#18247)
Implements the date range selection model in the individual range inputs.
1 parent ca03b8f commit fd99256

File tree

7 files changed

+97
-31
lines changed

7 files changed

+97
-31
lines changed

src/material/core/datetime/date-selection-model.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ export class DateRange<D> {
2626
readonly end: D | null) {}
2727
}
2828

29-
type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
29+
/**
30+
* Conditionally picks the date type, if a DateRange is passed in.
31+
* @docs-private
32+
*/
33+
export type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
3034

3135
/** Event emitted by the date selection model when its selection changes. */
3236
export interface DateSelectionModelChange<S> {
@@ -213,3 +217,17 @@ export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
213217
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
214218
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
215219
};
220+
221+
222+
/** @docs-private */
223+
export function MAT_RANGE_DATE_SELECTION_MODEL_FACTORY(
224+
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
225+
return parent || new MatRangeDateSelectionModel(adapter);
226+
}
227+
228+
/** Used to provide a range selection model to a component. */
229+
export const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
230+
provide: MatDateSelectionModel,
231+
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
232+
useFactory: MAT_RANGE_DATE_SELECTION_MODEL_FACTORY,
233+
};

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

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
DateAdapter,
3535
MatDateFormats,
3636
ErrorStateMatcher,
37+
DateRange,
38+
MatDateSelectionModel,
3739
} from '@angular/material/core';
3840
import {BooleanInput} from '@angular/cdk/coercion';
3941
import {MatDatepickerInputBase} from './datepicker-input-base';
@@ -58,8 +60,8 @@ export const MAT_DATE_RANGE_INPUT_PARENT =
5860
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
5961
*/
6062
@Directive()
61-
class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements OnInit, DoCheck {
62-
protected _validator: ValidatorFn | null;
63+
class MatDateRangeInputPartBase<D>
64+
extends MatDatepickerInputBase<DateRange<D>, D> implements OnInit, DoCheck {
6365

6466
/** @docs-private */
6567
ngControl: NgControl;
@@ -75,8 +77,13 @@ class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements
7577
@Optional() public _parentForm: NgForm,
7678
@Optional() public _parentFormGroup: FormGroupDirective,
7779
@Optional() dateAdapter: DateAdapter<D>,
78-
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
80+
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,
81+
82+
// TODO(crisbeto): this will be provided by the datepicker eventually.
83+
// We provide it here for the moment so we have something to test against.
84+
model: MatDateSelectionModel<DateRange<D>, D>) {
7985
super(elementRef, dateAdapter, dateFormats);
86+
super._registerModel(model);
8087
}
8188

8289
ngOnInit() {
@@ -123,9 +130,11 @@ class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements
123130
this._rangeInput._openDatepicker();
124131
}
125132

126-
protected _assignModelValue(_model: D | null): void {
127-
// TODO(crisbeto): implement
128-
}
133+
// Dummy property implementations since we can't pass an abstract class
134+
// into a mixin. These are overridden by the individual input classes.
135+
protected _validator: ValidatorFn | null;
136+
protected _assignValueToModel: (value: D | null) => void;
137+
protected _getValueFromModel: (modelValue: DateRange<D>) => D | null;
129138
}
130139

131140
const _MatDateRangeInputBase:
@@ -157,9 +166,16 @@ const _MatDateRangeInputBase:
157166
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
158167
]
159168
})
160-
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
169+
export class MatStartDate<D> extends _MatDateRangeInputBase<D>
170+
implements CanUpdateErrorState {
161171
// TODO(crisbeto): start-range-specific validators should go here.
162172
protected _validator = Validators.compose([this._parseValidator]);
173+
protected _getValueFromModel = (modelValue: DateRange<D>) => modelValue.start;
174+
protected _assignValueToModel = (value: D | null) => {
175+
if (this._model) {
176+
this._model.updateSelection(new DateRange(value, this._model.selection.end), this);
177+
}
178+
}
163179

164180
/** Gets the value that should be used when mirroring the input's size. */
165181
getMirrorValue(): string {
@@ -200,6 +216,12 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpd
200216
export class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
201217
// TODO(crisbeto): end-range-specific validators should go here.
202218
protected _validator = Validators.compose([this._parseValidator]);
219+
protected _getValueFromModel = (modelValue: DateRange<D>) => modelValue.end;
220+
protected _assignValueToModel = (value: D | null) => {
221+
if (this._model) {
222+
this._model.updateSelection(new DateRange(this._model.selection.start, value), this);
223+
}
224+
}
203225

204226
static ngAcceptInputType_disabled: BooleanInput;
205227
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
Self,
2020
} from '@angular/core';
2121
import {MatFormFieldControl, MatFormField} from '@angular/material/form-field';
22-
import {DateRange} from '@angular/material/core';
22+
import {DateRange, MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER} from '@angular/material/core';
2323
import {NgControl, ControlContainer} from '@angular/forms';
2424
import {Subject} from 'rxjs';
2525
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
@@ -49,6 +49,10 @@ let nextUniqueId = 0;
4949
providers: [
5050
{provide: MatFormFieldControl, useExisting: MatDateRangeInput},
5151
{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,
5256
]
5357
})
5458
export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,

src/material/datepicker/datepicker-input-base.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
MAT_DATE_FORMATS,
3131
MatDateFormats,
3232
MatDateSelectionModel,
33+
ExtractDateTypeFromSelection,
3334
} from '@angular/material/core';
3435
import {Subscription} from 'rxjs';
3536
import {createMissingDateImplError} from './datepicker-errors';
@@ -39,13 +40,13 @@ import {createMissingDateImplError} from './datepicker-errors';
3940
* input or change event because the event may have been triggered by the user clicking on the
4041
* calendar popup. For consistency, we always use MatDatepickerInputEvent instead.
4142
*/
42-
export class MatDatepickerInputEvent<D> {
43+
export class MatDatepickerInputEvent<D, S = unknown> {
4344
/** The new value for the target datepicker input. */
4445
value: D | null;
4546

4647
constructor(
4748
/** Reference to the datepicker input component that emitted the event. */
48-
public target: MatDatepickerInputBase<D>,
49+
public target: MatDatepickerInputBase<S, D>,
4950
/** Reference to the native input element associated with the datepicker input. */
5051
public targetElement: HTMLElement) {
5152
this.value = this.target.value;
@@ -54,11 +55,13 @@ export class MatDatepickerInputEvent<D> {
5455

5556
/** Base class for datepicker inputs. */
5657
@Directive()
57-
export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor, OnDestroy,
58-
Validator {
58+
export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection<S>>
59+
implements ControlValueAccessor, OnDestroy, Validator {
5960
/** The value of the input. */
6061
@Input()
61-
get value(): D | null { return this._model ? this._model.selection : this._pendingValue; }
62+
get value(): D | null {
63+
return this._model ? this._getValueFromModel(this._model.selection) : this._pendingValue;
64+
}
6265
set value(value: D | null) {
6366
value = this._dateAdapter.deserialize(value);
6467
this._lastValueValid = !value || this._dateAdapter.isValid(value);
@@ -71,7 +74,7 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
7174
this._valueChange.emit(value);
7275
}
7376
}
74-
protected _model: MatDateSelectionModel<D | null, D> | undefined;
77+
protected _model: MatDateSelectionModel<S, D> | undefined;
7578

7679
/** Whether the datepicker-input is disabled. */
7780
@Input()
@@ -96,12 +99,12 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
9699
private _disabled: boolean;
97100

98101
/** Emits when a `change` event is fired on this `<input>`. */
99-
@Output() readonly dateChange: EventEmitter<MatDatepickerInputEvent<D>> =
100-
new EventEmitter<MatDatepickerInputEvent<D>>();
102+
@Output() readonly dateChange: EventEmitter<MatDatepickerInputEvent<D, S>> =
103+
new EventEmitter<MatDatepickerInputEvent<D, S>>();
101104

102105
/** Emits when an `input` event is fired on this `<input>`. */
103-
@Output() readonly dateInput: EventEmitter<MatDatepickerInputEvent<D>> =
104-
new EventEmitter<MatDatepickerInputEvent<D>>();
106+
@Output() readonly dateInput: EventEmitter<MatDatepickerInputEvent<D, S>> =
107+
new EventEmitter<MatDatepickerInputEvent<D, S>>();
105108

106109
/** Emits when the value changes (either due to user input or programmatic change). */
107110
_valueChange = new EventEmitter<D | null>();
@@ -129,7 +132,7 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
129132
null : {'matDatepickerParse': {'text': this._elementRef.nativeElement.value}};
130133
}
131134

132-
protected _registerModel(model: MatDateSelectionModel<D | null, D>): void {
135+
protected _registerModel(model: MatDateSelectionModel<S, D>): void {
133136
this._model = model;
134137
this._valueChangesSubscription.unsubscribe();
135138

@@ -139,11 +142,12 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
139142

140143
this._valueChangesSubscription = this._model.selectionChanged.subscribe(event => {
141144
if (event.source !== this) {
142-
this._cvaOnChange(event.selection);
145+
const value = this._getValueFromModel(event.selection);
146+
this._cvaOnChange(value);
143147
this._onTouched();
144148
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
145149
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
146-
this._formatValue(event.selection);
150+
this._formatValue(value);
147151
}
148152
});
149153
}
@@ -152,7 +156,10 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
152156
protected abstract _openPopup(): void;
153157

154158
/** Assigns a value to the input's model. */
155-
protected abstract _assignModelValue(model: D | null): void;
159+
protected abstract _assignValueToModel(model: D | null): void;
160+
161+
/** Converts a value from the model into a native value for the input. */
162+
protected abstract _getValueFromModel(modelValue: S): D | null;
156163

157164
/** The combined form control validator for this input. */
158165
protected abstract _validator: ValidatorFn | null;
@@ -272,7 +279,7 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
272279
// We may get some incoming values before the model was
273280
// assigned. Save the value so that we can assign it later.
274281
if (this._model) {
275-
this._assignModelValue(value);
282+
this._assignValueToModel(value);
276283
this._pendingValue = null;
277284
} else {
278285
this._pendingValue = value;

src/material/datepicker/datepicker-input.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const MAT_DATEPICKER_VALIDATORS: any = {
6969
},
7070
exportAs: 'matDatepickerInput',
7171
})
72-
export class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
72+
export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> {
7373
/** The datepicker that this input is associated with. */
7474
@Input()
7575
set matDatepicker(datepicker: MatDatepicker<D>) {
@@ -170,7 +170,11 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
170170
}
171171
}
172172

173-
protected _assignModelValue(value: D | null): void {
173+
protected _getValueFromModel(modelValue: D | null): D | null {
174+
return modelValue;
175+
}
176+
177+
protected _assignValueToModel(value: D | null): void {
174178
if (this._model) {
175179
this._model.updateSelection(value, this);
176180
}

tools/public_api_guard/material/core.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ export declare class ErrorStateMatcher {
105105
static ɵprov: i0.ɵɵInjectableDef<ErrorStateMatcher>;
106106
}
107107

108+
export declare type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;
109+
108110
export declare const JAN = 0, FEB = 1, MAR = 2, APR = 3, MAY = 4, JUN = 5, JUL = 6, AUG = 7, SEP = 8, OCT = 9, NOV = 10, DEC = 11;
109111

110112
export declare type FloatLabelType = 'always' | 'never' | 'auto';
@@ -211,6 +213,10 @@ export declare const MAT_NATIVE_DATE_FORMATS: MatDateFormats;
211213

212214
export declare const MAT_OPTION_PARENT_COMPONENT: InjectionToken<MatOptionParentComponent>;
213215

216+
export declare function MAT_RANGE_DATE_SELECTION_MODEL_FACTORY(parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>): MatSingleDateSelectionModel<unknown>;
217+
218+
export declare const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider;
219+
214220
export declare const MAT_RIPPLE_GLOBAL_OPTIONS: InjectionToken<RippleGlobalOptions>;
215221

216222
export declare function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>): MatSingleDateSelectionModel<unknown>;

tools/public_api_guard/material/datepicker.d.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export declare class MatDatepickerContent<D> extends _MatDatepickerContentMixinB
156156
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerContent<any>>;
157157
}
158158

159-
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
159+
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> {
160160
_dateFilter: (date: D | null) => boolean;
161161
_datepicker: MatDatepicker<D>;
162162
protected _validator: ValidatorFn | null;
@@ -165,8 +165,9 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
165165
max: D | null;
166166
min: D | null;
167167
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField: MatFormField);
168-
protected _assignModelValue(value: D | null): void;
168+
protected _assignValueToModel(value: D | null): void;
169169
_getThemePalette(): ThemePalette;
170+
protected _getValueFromModel(modelValue: D | null): D | null;
170171
protected _openPopup(): void;
171172
getConnectedOverlayOrigin(): ElementRef;
172173
getPopupConnectionElementRef(): ElementRef;
@@ -176,12 +177,12 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
176177
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerInput<any>>;
177178
}
178179

179-
export declare class MatDatepickerInputEvent<D> {
180-
target: MatDatepickerInputBase<D>;
180+
export declare class MatDatepickerInputEvent<D, S = unknown> {
181+
target: MatDatepickerInputBase<S, D>;
181182
targetElement: HTMLElement;
182183
value: D | null;
183184
constructor(
184-
target: MatDatepickerInputBase<D>,
185+
target: MatDatepickerInputBase<S, D>,
185186
targetElement: HTMLElement);
186187
}
187188

@@ -265,6 +266,8 @@ export declare class MatDateRangeInput<D> implements MatFormFieldControl<DateRan
265266
}
266267

267268
export declare class MatEndDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
269+
protected _assignValueToModel: (value: D | null) => void;
270+
protected _getValueFromModel: (modelValue: DateRange<D>) => D | null;
268271
protected _validator: ValidatorFn | null;
269272
static ngAcceptInputType_disabled: BooleanInput;
270273
static ɵdir: i0.ɵɵDirectiveDefWithMeta<MatEndDate<any>, "input[matEndDate]", never, {}, {}, never>;
@@ -330,6 +333,8 @@ export declare class MatMultiYearView<D> implements AfterContentInit, OnDestroy
330333
}
331334

332335
export declare class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
336+
protected _assignValueToModel: (value: D | null) => void;
337+
protected _getValueFromModel: (modelValue: DateRange<D>) => D | null;
333338
protected _validator: ValidatorFn | null;
334339
getMirrorValue(): string;
335340
static ngAcceptInputType_disabled: BooleanInput;

0 commit comments

Comments
 (0)