Skip to content

refactor(datepicker): implement range selection model #18247

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 23, 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
20 changes: 19 additions & 1 deletion src/material/core/datetime/date-selection-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,11 @@ export class DateRange<D> {
readonly end: D | null) {}
}

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

/** Event emitted by the date selection model when its selection changes. */
export interface DateSelectionModelChange<S> {
Expand Down Expand Up @@ -213,3 +217,17 @@ export const MAT_SINGLE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
useFactory: MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY,
};


/** @docs-private */
export function MAT_RANGE_DATE_SELECTION_MODEL_FACTORY(
parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>) {
return parent || new MatRangeDateSelectionModel(adapter);
}

/** Used to provide a range selection model to a component. */
export const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider = {
provide: MatDateSelectionModel,
deps: [[new Optional(), new SkipSelf(), MatDateSelectionModel], DateAdapter],
useFactory: MAT_RANGE_DATE_SELECTION_MODEL_FACTORY,
};
36 changes: 29 additions & 7 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ import {
DateAdapter,
MatDateFormats,
ErrorStateMatcher,
DateRange,
MatDateSelectionModel,
} from '@angular/material/core';
import {BooleanInput} from '@angular/cdk/coercion';
import {MatDatepickerInputBase} from './datepicker-input-base';
Expand All @@ -58,8 +60,8 @@ export const MAT_DATE_RANGE_INPUT_PARENT =
* Base class for the individual inputs that can be projected inside a `mat-date-range-input`.
*/
@Directive()
class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements OnInit, DoCheck {
protected _validator: ValidatorFn | null;
class MatDateRangeInputPartBase<D>
extends MatDatepickerInputBase<DateRange<D>, D> implements OnInit, DoCheck {

/** @docs-private */
ngControl: NgControl;
Expand All @@ -75,8 +77,13 @@ class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements
@Optional() public _parentForm: NgForm,
@Optional() public _parentFormGroup: FormGroupDirective,
@Optional() dateAdapter: DateAdapter<D>,
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats,

// TODO(crisbeto): this will be provided by the datepicker eventually.
// We provide it here for the moment so we have something to test against.
model: MatDateSelectionModel<DateRange<D>, D>) {
super(elementRef, dateAdapter, dateFormats);
super._registerModel(model);
}

ngOnInit() {
Expand Down Expand Up @@ -123,9 +130,11 @@ class MatDateRangeInputPartBase<D> extends MatDatepickerInputBase<D> implements
this._rangeInput._openDatepicker();
}

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

const _MatDateRangeInputBase:
Expand Down Expand Up @@ -157,9 +166,16 @@ const _MatDateRangeInputBase:
{provide: NG_VALIDATORS, useExisting: MatStartDate, multi: true}
]
})
export class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
export class MatStartDate<D> extends _MatDateRangeInputBase<D>
implements CanUpdateErrorState {
// TODO(crisbeto): start-range-specific validators should go here.
protected _validator = Validators.compose([this._parseValidator]);
protected _getValueFromModel = (modelValue: DateRange<D>) => modelValue.start;
protected _assignValueToModel = (value: D | null) => {
if (this._model) {
this._model.updateSelection(new DateRange(value, this._model.selection.end), this);
}
}

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

static ngAcceptInputType_disabled: BooleanInput;
}
6 changes: 5 additions & 1 deletion src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
Self,
} from '@angular/core';
import {MatFormFieldControl, MatFormField} from '@angular/material/form-field';
import {DateRange} from '@angular/material/core';
import {DateRange, MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER} from '@angular/material/core';
import {NgControl, ControlContainer} from '@angular/forms';
import {Subject} from 'rxjs';
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
Expand Down Expand Up @@ -49,6 +49,10 @@ let nextUniqueId = 0;
providers: [
{provide: MatFormFieldControl, useExisting: MatDateRangeInput},
{provide: MAT_DATE_RANGE_INPUT_PARENT, useExisting: MatDateRangeInput},

// TODO(crisbeto): this will be provided by the datepicker eventually.
// We provide it here for the moment so we have something to test against.
MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER,
]
})
export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
Expand Down
37 changes: 22 additions & 15 deletions src/material/datepicker/datepicker-input-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
MAT_DATE_FORMATS,
MatDateFormats,
MatDateSelectionModel,
ExtractDateTypeFromSelection,
} from '@angular/material/core';
import {Subscription} from 'rxjs';
import {createMissingDateImplError} from './datepicker-errors';
Expand All @@ -39,13 +40,13 @@ import {createMissingDateImplError} from './datepicker-errors';
* input or change event because the event may have been triggered by the user clicking on the
* calendar popup. For consistency, we always use MatDatepickerInputEvent instead.
*/
export class MatDatepickerInputEvent<D> {
export class MatDatepickerInputEvent<D, S = unknown> {
/** The new value for the target datepicker input. */
value: D | null;

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

/** Base class for datepicker inputs. */
@Directive()
export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor, OnDestroy,
Validator {
export abstract class MatDatepickerInputBase<S, D = ExtractDateTypeFromSelection<S>>
implements ControlValueAccessor, OnDestroy, Validator {
/** The value of the input. */
@Input()
get value(): D | null { return this._model ? this._model.selection : this._pendingValue; }
get value(): D | null {
return this._model ? this._getValueFromModel(this._model.selection) : this._pendingValue;
}
set value(value: D | null) {
value = this._dateAdapter.deserialize(value);
this._lastValueValid = !value || this._dateAdapter.isValid(value);
Expand All @@ -71,7 +74,7 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
this._valueChange.emit(value);
}
}
protected _model: MatDateSelectionModel<D | null, D> | undefined;
protected _model: MatDateSelectionModel<S, D> | undefined;

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

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

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

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

protected _registerModel(model: MatDateSelectionModel<D | null, D>): void {
protected _registerModel(model: MatDateSelectionModel<S, D>): void {
this._model = model;
this._valueChangesSubscription.unsubscribe();

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

this._valueChangesSubscription = this._model.selectionChanged.subscribe(event => {
if (event.source !== this) {
this._cvaOnChange(event.selection);
const value = this._getValueFromModel(event.selection);
this._cvaOnChange(value);
this._onTouched();
this.dateInput.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
this.dateChange.emit(new MatDatepickerInputEvent(this, this._elementRef.nativeElement));
this._formatValue(event.selection);
this._formatValue(value);
}
});
}
Expand All @@ -152,7 +156,10 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
protected abstract _openPopup(): void;

/** Assigns a value to the input's model. */
protected abstract _assignModelValue(model: D | null): void;
protected abstract _assignValueToModel(model: D | null): void;

/** Converts a value from the model into a native value for the input. */
protected abstract _getValueFromModel(modelValue: S): D | null;

/** The combined form control validator for this input. */
protected abstract _validator: ValidatorFn | null;
Expand Down Expand Up @@ -272,7 +279,7 @@ export abstract class MatDatepickerInputBase<D> implements ControlValueAccessor,
// We may get some incoming values before the model was
// assigned. Save the value so that we can assign it later.
if (this._model) {
this._assignModelValue(value);
this._assignValueToModel(value);
this._pendingValue = null;
} else {
this._pendingValue = value;
Expand Down
8 changes: 6 additions & 2 deletions src/material/datepicker/datepicker-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export const MAT_DATEPICKER_VALIDATORS: any = {
},
exportAs: 'matDatepickerInput',
})
export class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
export class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> {
/** The datepicker that this input is associated with. */
@Input()
set matDatepicker(datepicker: MatDatepicker<D>) {
Expand Down Expand Up @@ -170,7 +170,11 @@ export class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
}
}

protected _assignModelValue(value: D | null): void {
protected _getValueFromModel(modelValue: D | null): D | null {
return modelValue;
}

protected _assignValueToModel(value: D | null): void {
if (this._model) {
this._model.updateSelection(value, this);
}
Expand Down
6 changes: 6 additions & 0 deletions tools/public_api_guard/material/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ export declare class ErrorStateMatcher {
static ɵprov: i0.ɵɵInjectableDef<ErrorStateMatcher>;
}

export declare type ExtractDateTypeFromSelection<T> = T extends DateRange<infer D> ? D : NonNullable<T>;

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;

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

export declare const MAT_OPTION_PARENT_COMPONENT: InjectionToken<MatOptionParentComponent>;

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

export declare const MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER: FactoryProvider;

export declare const MAT_RIPPLE_GLOBAL_OPTIONS: InjectionToken<RippleGlobalOptions>;

export declare function MAT_SINGLE_DATE_SELECTION_MODEL_FACTORY(parent: MatSingleDateSelectionModel<unknown>, adapter: DateAdapter<unknown>): MatSingleDateSelectionModel<unknown>;
Expand Down
15 changes: 10 additions & 5 deletions tools/public_api_guard/material/datepicker.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export declare class MatDatepickerContent<D> extends _MatDatepickerContentMixinB
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerContent<any>>;
}

export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D | null, D> {
_dateFilter: (date: D | null) => boolean;
_datepicker: MatDatepicker<D>;
protected _validator: ValidatorFn | null;
Expand All @@ -165,8 +165,9 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
max: D | null;
min: D | null;
constructor(elementRef: ElementRef<HTMLInputElement>, dateAdapter: DateAdapter<D>, dateFormats: MatDateFormats, _formField: MatFormField);
protected _assignModelValue(value: D | null): void;
protected _assignValueToModel(value: D | null): void;
_getThemePalette(): ThemePalette;
protected _getValueFromModel(modelValue: D | null): D | null;
protected _openPopup(): void;
getConnectedOverlayOrigin(): ElementRef;
getPopupConnectionElementRef(): ElementRef;
Expand All @@ -176,12 +177,12 @@ export declare class MatDatepickerInput<D> extends MatDatepickerInputBase<D> {
static ɵfac: i0.ɵɵFactoryDef<MatDatepickerInput<any>>;
}

export declare class MatDatepickerInputEvent<D> {
target: MatDatepickerInputBase<D>;
export declare class MatDatepickerInputEvent<D, S = unknown> {
target: MatDatepickerInputBase<S, D>;
targetElement: HTMLElement;
value: D | null;
constructor(
target: MatDatepickerInputBase<D>,
target: MatDatepickerInputBase<S, D>,
targetElement: HTMLElement);
}

Expand Down Expand Up @@ -265,6 +266,8 @@ export declare class MatDateRangeInput<D> implements MatFormFieldControl<DateRan
}

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

export declare class MatStartDate<D> extends _MatDateRangeInputBase<D> implements CanUpdateErrorState {
protected _assignValueToModel: (value: D | null) => void;
protected _getValueFromModel: (modelValue: DateRange<D>) => D | null;
protected _validator: ValidatorFn | null;
getMirrorValue(): string;
static ngAcceptInputType_disabled: BooleanInput;
Expand Down