/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {Directive, ElementRef, forwardRef, Input, OnDestroy, signal, inject} from '@angular/core'; import {NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidatorFn, Validators} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; import {MAT_FORM_FIELD} from '@angular/material/form-field'; import {MAT_INPUT_VALUE_ACCESSOR} from '@angular/material/input'; import {Subscription} from 'rxjs'; import {DateSelectionModelChange} from './date-selection-model'; import {MatDatepickerControl, MatDatepickerPanel} from './datepicker-base'; import {_MatFormFieldPartial, DateFilterFn, MatDatepickerInputBase} from './datepicker-input-base'; /** @docs-private */ export const MAT_DATEPICKER_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => MatDatepickerInput), multi: true, }; /** @docs-private */ export const MAT_DATEPICKER_VALIDATORS: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => MatDatepickerInput), multi: true, }; /** Directive used to connect an input to a MatDatepicker. */ @Directive({ selector: 'input[matDatepicker]', providers: [ MAT_DATEPICKER_VALUE_ACCESSOR, MAT_DATEPICKER_VALIDATORS, {provide: MAT_INPUT_VALUE_ACCESSOR, useExisting: MatDatepickerInput}, ], host: { 'class': 'mat-datepicker-input', '[attr.aria-haspopup]': '_datepicker ? "dialog" : null', '[attr.aria-owns]': '_ariaOwns()', '[attr.min]': 'min ? _dateAdapter.toIso8601(min) : null', '[attr.max]': 'max ? _dateAdapter.toIso8601(max) : null', // Used by the test harness to tie this input to its calendar. We can't depend on // `aria-owns` for this, because it's only defined while the calendar is open. '[attr.data-mat-calendar]': '_datepicker ? _datepicker.id : null', '[disabled]': 'disabled', '(input)': '_onInput($event.target.value)', '(change)': '_onChange()', '(blur)': '_onBlur()', '(keydown)': '_onKeydown($event)', }, exportAs: 'matDatepickerInput', }) export class MatDatepickerInput extends MatDatepickerInputBase implements MatDatepickerControl, OnDestroy { private _formField = inject<_MatFormFieldPartial>(MAT_FORM_FIELD, {optional: true}); private _closedSubscription = Subscription.EMPTY; private _openedSubscription = Subscription.EMPTY; /** The datepicker that this input is associated with. */ @Input() set matDatepicker(datepicker: MatDatepickerPanel, D | null, D>) { if (datepicker) { this._datepicker = datepicker; this._ariaOwns.set(datepicker.opened ? datepicker.id : null); this._closedSubscription = datepicker.closedStream.subscribe(() => { this._onTouched(); this._ariaOwns.set(null); }); this._openedSubscription = datepicker.openedStream.subscribe(() => { this._ariaOwns.set(datepicker.id); }); this._registerModel(datepicker.registerInput(this)); } } _datepicker: MatDatepickerPanel, D | null, D>; /** The id of the panel owned by this input. */ protected _ariaOwns = signal(null); /** The minimum valid date. */ @Input() get min(): D | null { return this._min; } set min(value: D | null) { const validValue = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value)); if (!this._dateAdapter.sameDate(validValue, this._min)) { this._min = validValue; this._validatorOnChange(); } } private _min: D | null; /** The maximum valid date. */ @Input() get max(): D | null { return this._max; } set max(value: D | null) { const validValue = this._dateAdapter.getValidDateOrNull(this._dateAdapter.deserialize(value)); if (!this._dateAdapter.sameDate(validValue, this._max)) { this._max = validValue; this._validatorOnChange(); } } private _max: D | null; /** Function that can be used to filter out dates within the datepicker. */ @Input('matDatepickerFilter') get dateFilter() { return this._dateFilter; } set dateFilter(value: DateFilterFn) { const wasMatchingValue = this._matchesFilter(this.value); this._dateFilter = value; if (this._matchesFilter(this.value) !== wasMatchingValue) { this._validatorOnChange(); } } private _dateFilter: DateFilterFn; /** The combined form control validator for this input. */ protected _validator: ValidatorFn | null; constructor(...args: unknown[]); constructor() { super(); this._validator = Validators.compose(super._getValidators()); } /** * Gets the element that the datepicker popup should be connected to. * @return The element to connect the popup to. */ getConnectedOverlayOrigin(): ElementRef { return this._formField ? this._formField.getConnectedOverlayOrigin() : this._elementRef; } /** Gets the ID of an element that should be used a description for the calendar overlay. */ getOverlayLabelId(): string | null { if (this._formField) { return this._formField.getLabelId(); } return this._elementRef.nativeElement.getAttribute('aria-labelledby'); } /** Returns the palette used by the input's form field, if any. */ getThemePalette(): ThemePalette { return this._formField ? this._formField.color : undefined; } /** Gets the value at which the calendar should start. */ getStartValue(): D | null { return this.value; } override ngOnDestroy() { super.ngOnDestroy(); this._closedSubscription.unsubscribe(); this._openedSubscription.unsubscribe(); } /** Opens the associated datepicker. */ protected _openPopup(): void { if (this._datepicker) { this._datepicker.open(); } } protected _getValueFromModel(modelValue: D | null): D | null { return modelValue; } protected _assignValueToModel(value: D | null): void { if (this._model) { this._model.updateSelection(value, this); } } /** Gets the input's minimum date. */ _getMinDate() { return this._min; } /** Gets the input's maximum date. */ _getMaxDate() { return this._max; } /** Gets the input's date filtering function. */ protected _getDateFilter() { return this._dateFilter; } protected _shouldHandleChangeEvent(event: DateSelectionModelChange) { return event.source !== this; } }