Skip to content

refactor(datepicker): set up date range picker classes #18292

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 29, 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
15 changes: 9 additions & 6 deletions src/dev-app/datepicker/datepicker-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -175,35 +175,38 @@ <h2>Range picker</h2>
<div class="demo-range-group">
<mat-form-field>
<mat-label>Enter a date range</mat-label>
<mat-date-range-input [formGroup]="range1">
<mat-date-range-input [formGroup]="range1" [rangePicker]="range1Picker">
<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-datepicker-toggle [for]="range1Picker" matSuffix></mat-datepicker-toggle>
<mat-date-range-picker #range1Picker></mat-date-range-picker>
</mat-form-field>
<div>{{range1.value | json}}</div>
</div>

<div class="demo-range-group">
<mat-form-field appearance="fill">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input [formGroup]="range2">
<mat-date-range-input [formGroup]="range2" [rangePicker]="range2Picker">
<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-datepicker-toggle [for]="range2Picker" matSuffix></mat-datepicker-toggle>
<mat-date-range-picker #range2Picker></mat-date-range-picker>
</mat-form-field>
<div>{{range2.value | json}}</div>
</div>

<div class="demo-range-group">
<mat-form-field appearance="outline">
<mat-label>Enter a date range</mat-label>
<mat-date-range-input [formGroup]="range3">
<mat-date-range-input [formGroup]="range3" [rangePicker]="range3Picker">
<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-datepicker-toggle [for]="range3Picker" matSuffix></mat-datepicker-toggle>
<mat-date-range-picker #range3Picker></mat-date-range-picker>
</mat-form-field>
<div>{{range3.value | json}}</div>
</div>
50 changes: 34 additions & 16 deletions src/material/datepicker/date-range-input-parts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ import {
MatDateFormats,
ErrorStateMatcher,
DateRange,
MatDateSelectionModel,
} from '@angular/material/core';
import {BooleanInput} from '@angular/cdk/coercion';
import {MatDatepickerInputBase} from './datepicker-input-base';
import {MatDatepickerInputBase, DateFilterFn} from './datepicker-input-base';

/** Parent component that should be wrapped around `MatStartDate` and `MatEndDate`. */
export interface MatDateRangeInputParent {
export interface MatDateRangeInputParent<D> {
id: string;
min: D | null;
max: D | null;
dateFilter: DateFilterFn<D>;
_ariaDescribedBy: string | null;
_ariaLabelledBy: string | null;
_handleChildValueChange: () => void;
Expand All @@ -54,14 +56,14 @@ export interface MatDateRangeInputParent {
* to the parts without circular dependencies.
*/
export const MAT_DATE_RANGE_INPUT_PARENT =
new InjectionToken<MatDateRangeInputParent>('MAT_DATE_RANGE_INPUT_PARENT');
new InjectionToken<MatDateRangeInputParent<unknown>>('MAT_DATE_RANGE_INPUT_PARENT');

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

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

constructor(
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent,
@Inject(MAT_DATE_RANGE_INPUT_PARENT) public _rangeInput: MatDateRangeInputParent<D>,
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,

// 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>) {
@Optional() @Inject(MAT_DATE_FORMATS) dateFormats: MatDateFormats) {
super(elementRef, dateAdapter, dateFormats);
super._registerModel(model);
}

ngOnInit() {
Expand Down Expand Up @@ -133,6 +130,21 @@ abstract class MatDateRangeInputPartBase<D>
protected _openPopup(): void {
this._rangeInput._openDatepicker();
}

/** Gets the minimum date from the range input. */
protected _getMinDate() {
return this._rangeInput.min;
}

/** Gets the maximum date from the range input. */
protected _getMaxDate() {
return this._rangeInput.max;
}

/** Gets the date filter function from the range input. */
protected _getDateFilter() {
return this._rangeInput.dateFilter;
}
}

const _MatDateRangeInputBase:
Expand Down Expand Up @@ -165,10 +177,9 @@ 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 _validator = Validators.compose(super._getValidators());

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

protected _formatValue(value: D | null) {
super._formatValue(value);

// Any time the input value is reformatted we need to tell the parent.
this._rangeInput._handleChildValueChange();
}

/** Gets the value that should be used when mirroring the input's size. */
getMirrorValue(): string {
const element = this._elementRef.nativeElement;
Expand Down Expand Up @@ -218,7 +236,7 @@ export class MatStartDate<D> extends _MatDateRangeInputBase<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]);
protected _validator = Validators.compose(super._getValidators());

protected _getValueFromModel(modelValue: DateRange<D>) {
return modelValue.end;
Expand Down
147 changes: 129 additions & 18 deletions src/material/datepicker/date-range-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,28 @@ import {
AfterContentInit,
ChangeDetectorRef,
Self,
ElementRef,
} from '@angular/core';
import {MatFormFieldControl, MatFormField} from '@angular/material/form-field';
import {DateRange, MAT_RANGE_DATE_SELECTION_MODEL_PROVIDER} from '@angular/material/core';
import {
DateRange,
ThemePalette,
DateAdapter,
MatDateSelectionModel,
} from '@angular/material/core';
import {NgControl, ControlContainer} from '@angular/forms';
import {Subject} from 'rxjs';
import {Subject, merge} from 'rxjs';
import {coerceBooleanProperty, BooleanInput} from '@angular/cdk/coercion';
import {
MatStartDate,
MatEndDate,
MatDateRangeInputParent,
MAT_DATE_RANGE_INPUT_PARENT,
} from './date-range-input-parts';
import {MatDatepickerControl} from './datepicker-base';
import {createMissingDateImplError} from './datepicker-errors';
import {DateFilterFn} from './datepicker-input-base';
import {MatDateRangePicker} from './date-range-picker';

let nextUniqueId = 0;

Expand All @@ -49,16 +59,14 @@ 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>>,
MatDateRangeInputParent, AfterContentInit, OnDestroy {
MatDatepickerControl<D>, MatDateRangeInputParent<D>, AfterContentInit, OnDestroy {
/** Current value of the range input. */
value: DateRange<D> | null = null;
get value() {
return this._model ? this._model.selection : null;
}

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

/**
* Implemented as a part of `MatFormFieldControl`, but not used.
* Use `startPlaceholder` and `endPlaceholder` instead.
* Set the placeholder attribute on `matStartDate` and `matEndDate`.
* @docs-private
*/
placeholder: string;

/** The range picker that this input is associated with. */
@Input()
get rangePicker() { return this._rangePicker; }
set rangePicker(rangePicker: MatDateRangePicker<D>) {
if (rangePicker) {
this._model = rangePicker._registerInput(this);
this._rangePicker = rangePicker;
this._registerModel(this._model!);
}
}
private _rangePicker: MatDateRangePicker<D>;

/** Whether the input is required. */
@Input()
get required(): boolean { return !!this._required; }
Expand All @@ -92,6 +112,33 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
}
private _required: boolean;

/** Function that can be used to filter out dates within the date range picker. */
@Input()
get dateFilter() { return this._dateFilter; }
set dateFilter(value: DateFilterFn<D>) {
this._dateFilter = value;
this._revalidate();
}
private _dateFilter: DateFilterFn<D>;

/** The minimum valid date. */
@Input()
get min(): D | null { return this._min; }
set min(value: D | null) {
this._min = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._revalidate();
}
private _min: D | null;

/** The maximum valid date. */
@Input()
get max(): D | null { return this._max; }
set max(value: D | null) {
this._max = this._getValidDateOrNull(this._dateAdapter.deserialize(value));
this._revalidate();
}
private _max: D | null;

/** Whether the input is disabled. */
get disabled(): boolean {
if (this._startInput && this._endInput) {
Expand Down Expand Up @@ -123,11 +170,8 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
/** Value for the `aria-labelledby` attribute of the inputs. */
_ariaLabelledBy: string | null = null;

/** Placeholder for the start input. */
@Input() startPlaceholder: string;

/** Placeholder for the end input. */
@Input() endPlaceholder: string;
/** Date selection model currently registered with the input. */
private _model: MatDateSelectionModel<DateRange<D>> | undefined;

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

/** Emits when the input's disabled state changes. */
_disabledChange = new Subject<boolean>();

constructor(
private _changeDetectorRef: ChangeDetectorRef,
private _elementRef: ElementRef<HTMLElement>,
@Optional() @Self() control: ControlContainer,
@Optional() formField?: MatFormField) {
@Optional() private _dateAdapter: DateAdapter<D>,
@Optional() private _formField?: MatFormField) {

if (!_dateAdapter) {
throw createMissingDateImplError('DateAdapter');
}

// TODO(crisbeto): remove `as any` after #18206 lands.
this.ngControl = control as any;
this._ariaLabelledBy = formField ? formField._labelId : null;
this._ariaLabelledBy = _formField ? _formField._labelId : null;
}

/**
Expand Down Expand Up @@ -179,10 +232,36 @@ export class MatDateRangeInput<D> implements MatFormFieldControl<DateRange<D>>,
if (!this._endInput) {
throw Error('mat-date-range-input must contain a matEndDate input');
}

if (this._model) {
this._registerModel(this._model);
}

// We don't need to unsubscribe from this, because we
// know that the input streams will be completed on destroy.
merge(this._startInput._disabledChange, this._endInput._disabledChange).subscribe(() => {
this._disabledChange.next(this.disabled);
});
}

ngOnDestroy() {
this.stateChanges.complete();
this._disabledChange.unsubscribe();
}

/** Gets the date at which the calendar should start. */
getStartValue(): D | null {
return this.value ? this.value.start : null;
}

/** Gets the input's theme palette. */
getThemePalette(): ThemePalette {
return this._formField ? this._formField.color : undefined;
}

/** Gets the element to which the calendar overlay should be attached. */
getConnectedOverlayOrigin(): ElementRef {
return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef;
}

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

/** Opens the datepicker associated with the input. */
/** Opens the date range picker associated with the input. */
_openDatepicker() {
// TODO(crisbeto): implement once the datepicker is in place.
if (this._rangePicker) {
this._rangePicker.open();
}
}

/**
* @param obj The object to check.
* @returns The given object if it is both a date instance and valid, otherwise null.
*/
private _getValidDateOrNull(obj: any): D | null {
return (this._dateAdapter.isDateInstance(obj) && this._dateAdapter.isValid(obj)) ? obj : null;
}

/** Re-runs the validators on the start/end inputs. */
private _revalidate() {
if (this._startInput) {
this._startInput._validatorOnChange();
}

if (this._endInput) {
this._endInput._validatorOnChange();
}
}

/** Registers the current date selection model with the start/end inputs. */
private _registerModel(model: MatDateSelectionModel<DateRange<D>>) {
if (this._startInput) {
this._startInput._registerModel(model);
}

if (this._endInput) {
this._endInput._registerModel(model);
}
}

static ngAcceptInputType_required: BooleanInput;
Expand Down
Loading