UNPKG

@angular/material

Version:
974 lines (963 loc) 135 kB
import { Directionality } from '@angular/cdk/bidi'; import { coerceBooleanProperty } from '@angular/cdk/coercion'; import { Platform } from '@angular/cdk/platform'; import { NgTemplateOutlet } from '@angular/common'; import * as i0 from '@angular/core'; import { Directive, InjectionToken, inject, Input, ElementRef, NgZone, Renderer2, Component, ChangeDetectionStrategy, ViewEncapsulation, ViewChild, ChangeDetectorRef, Injector, contentChild, ANIMATION_MODULE_TYPE, computed, afterRender, ContentChild, ContentChildren } from '@angular/core'; import { _IdGenerator } from '@angular/cdk/a11y'; import { Subscription, Subject, merge } from 'rxjs'; import { startWith, map, pairwise, filter, takeUntil } from 'rxjs/operators'; import { SharedResizeObserver } from '@angular/cdk/observers/private'; /** The floating label for a `mat-form-field`. */ class MatLabel { static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatLabel, isStandalone: true, selector: "mat-label", ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatLabel, decorators: [{ type: Directive, args: [{ selector: 'mat-label', }] }] }); /** * Injection token that can be used to reference instances of `MatError`. It serves as * alternative token to the actual `MatError` class which could cause unnecessary * retention of the class and its directive metadata. */ const MAT_ERROR = new InjectionToken('MatError'); /** Single error message to be shown underneath the form-field. */ class MatError { id = inject(_IdGenerator).getId('mat-mdc-error-'); constructor() { } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatError, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatError, isStandalone: true, selector: "mat-error, [matError]", inputs: { id: "id" }, host: { properties: { "id": "id" }, classAttribute: "mat-mdc-form-field-error mat-mdc-form-field-bottom-align" }, providers: [{ provide: MAT_ERROR, useExisting: MatError }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatError, decorators: [{ type: Directive, args: [{ selector: 'mat-error, [matError]', host: { 'class': 'mat-mdc-form-field-error mat-mdc-form-field-bottom-align', '[id]': 'id', }, providers: [{ provide: MAT_ERROR, useExisting: MatError }], }] }], ctorParameters: () => [], propDecorators: { id: [{ type: Input }] } }); /** Hint text to be shown underneath the form field control. */ class MatHint { /** Whether to align the hint label at the start or end of the line. */ align = 'start'; /** Unique ID for the hint. Used for the aria-describedby on the form field control. */ id = inject(_IdGenerator).getId('mat-mdc-hint-'); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatHint, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatHint, isStandalone: true, selector: "mat-hint", inputs: { align: "align", id: "id" }, host: { properties: { "class.mat-mdc-form-field-hint-end": "align === \"end\"", "id": "id", "attr.align": "null" }, classAttribute: "mat-mdc-form-field-hint mat-mdc-form-field-bottom-align" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatHint, decorators: [{ type: Directive, args: [{ selector: 'mat-hint', host: { 'class': 'mat-mdc-form-field-hint mat-mdc-form-field-bottom-align', '[class.mat-mdc-form-field-hint-end]': 'align === "end"', '[id]': 'id', // Remove align attribute to prevent it from interfering with layout. '[attr.align]': 'null', }, }] }], propDecorators: { align: [{ type: Input }], id: [{ type: Input }] } }); /** * Injection token that can be used to reference instances of `MatPrefix`. It serves as * alternative token to the actual `MatPrefix` class which could cause unnecessary * retention of the class and its directive metadata. */ const MAT_PREFIX = new InjectionToken('MatPrefix'); /** Prefix to be placed in front of the form field. */ class MatPrefix { set _isTextSelector(value) { this._isText = true; } _isText = false; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatPrefix, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatPrefix, isStandalone: true, selector: "[matPrefix], [matIconPrefix], [matTextPrefix]", inputs: { _isTextSelector: ["matTextPrefix", "_isTextSelector"] }, providers: [{ provide: MAT_PREFIX, useExisting: MatPrefix }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatPrefix, decorators: [{ type: Directive, args: [{ selector: '[matPrefix], [matIconPrefix], [matTextPrefix]', providers: [{ provide: MAT_PREFIX, useExisting: MatPrefix }], }] }], propDecorators: { _isTextSelector: [{ type: Input, args: ['matTextPrefix'] }] } }); /** * Injection token that can be used to reference instances of `MatSuffix`. It serves as * alternative token to the actual `MatSuffix` class which could cause unnecessary * retention of the class and its directive metadata. */ const MAT_SUFFIX = new InjectionToken('MatSuffix'); /** Suffix to be placed at the end of the form field. */ class MatSuffix { set _isTextSelector(value) { this._isText = true; } _isText = false; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatSuffix, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatSuffix, isStandalone: true, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: { _isTextSelector: ["matTextSuffix", "_isTextSelector"] }, providers: [{ provide: MAT_SUFFIX, useExisting: MatSuffix }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatSuffix, decorators: [{ type: Directive, args: [{ selector: '[matSuffix], [matIconSuffix], [matTextSuffix]', providers: [{ provide: MAT_SUFFIX, useExisting: MatSuffix }], }] }], propDecorators: { _isTextSelector: [{ type: Input, args: ['matTextSuffix'] }] } }); /** An injion token for the parent form-field. */ const FLOATING_LABEL_PARENT = new InjectionToken('FloatingLabelParent'); /** * Internal directive that maintains a MDC floating label. This directive does not * use the `MDCFloatingLabelFoundation` class, as it is not worth the size cost of * including it just to measure the label width and toggle some classes. * * The use of a directive allows us to conditionally render a floating label in the * template without having to manually manage instantiation and destruction of the * floating label component based on. * * The component is responsible for setting up the floating label styles, measuring label * width for the outline notch, and providing inputs that can be used to toggle the * label's floating or required state. */ class MatFormFieldFloatingLabel { _elementRef = inject(ElementRef); /** Whether the label is floating. */ get floating() { return this._floating; } set floating(value) { this._floating = value; if (this.monitorResize) { this._handleResize(); } } _floating = false; /** Whether to monitor for resize events on the floating label. */ get monitorResize() { return this._monitorResize; } set monitorResize(value) { this._monitorResize = value; if (this._monitorResize) { this._subscribeToResize(); } else { this._resizeSubscription.unsubscribe(); } } _monitorResize = false; /** The shared ResizeObserver. */ _resizeObserver = inject(SharedResizeObserver); /** The Angular zone. */ _ngZone = inject(NgZone); /** The parent form-field. */ _parent = inject(FLOATING_LABEL_PARENT); /** The current resize event subscription. */ _resizeSubscription = new Subscription(); constructor() { } ngOnDestroy() { this._resizeSubscription.unsubscribe(); } /** Gets the width of the label. Used for the outline notch. */ getWidth() { return estimateScrollWidth(this._elementRef.nativeElement); } /** Gets the HTML element for the floating label. */ get element() { return this._elementRef.nativeElement; } /** Handles resize events from the ResizeObserver. */ _handleResize() { // In the case where the label grows in size, the following sequence of events occurs: // 1. The label grows by 1px triggering the ResizeObserver // 2. The notch is expanded to accommodate the entire label // 3. The label expands to its full width, triggering the ResizeObserver again // // This is expected, but If we allow this to all happen within the same macro task it causes an // error: `ResizeObserver loop limit exceeded`. Therefore we push the notch resize out until // the next macro task. setTimeout(() => this._parent._handleLabelResized()); } /** Subscribes to resize events. */ _subscribeToResize() { this._resizeSubscription.unsubscribe(); this._ngZone.runOutsideAngular(() => { this._resizeSubscription = this._resizeObserver .observe(this._elementRef.nativeElement, { box: 'border-box' }) .subscribe(() => this._handleResize()); }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldFloatingLabel, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatFormFieldFloatingLabel, isStandalone: true, selector: "label[matFormFieldFloatingLabel]", inputs: { floating: "floating", monitorResize: "monitorResize" }, host: { properties: { "class.mdc-floating-label--float-above": "floating" }, classAttribute: "mdc-floating-label mat-mdc-floating-label" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldFloatingLabel, decorators: [{ type: Directive, args: [{ selector: 'label[matFormFieldFloatingLabel]', host: { 'class': 'mdc-floating-label mat-mdc-floating-label', '[class.mdc-floating-label--float-above]': 'floating', }, }] }], ctorParameters: () => [], propDecorators: { floating: [{ type: Input }], monitorResize: [{ type: Input }] } }); /** * Estimates the scroll width of an element. * via https://github.com/material-components/material-components-web/blob/c0a11ef0d000a098fd0c372be8f12d6a99302855/packages/mdc-dom/ponyfill.ts */ function estimateScrollWidth(element) { // Check the offsetParent. If the element inherits display: none from any // parent, the offsetParent property will be null (see // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent). // This check ensures we only clone the node when necessary. const htmlEl = element; if (htmlEl.offsetParent !== null) { return htmlEl.scrollWidth; } const clone = htmlEl.cloneNode(true); clone.style.setProperty('position', 'absolute'); clone.style.setProperty('transform', 'translate(-9999px, -9999px)'); document.documentElement.appendChild(clone); const scrollWidth = clone.scrollWidth; clone.remove(); return scrollWidth; } /** Class added when the line ripple is active. */ const ACTIVATE_CLASS = 'mdc-line-ripple--active'; /** Class added when the line ripple is being deactivated. */ const DEACTIVATING_CLASS = 'mdc-line-ripple--deactivating'; /** * Internal directive that creates an instance of the MDC line-ripple component. Using a * directive allows us to conditionally render a line-ripple in the template without having * to manually create and destroy the `MDCLineRipple` component whenever the condition changes. * * The directive sets up the styles for the line-ripple and provides an API for activating * and deactivating the line-ripple. */ class MatFormFieldLineRipple { _elementRef = inject(ElementRef); _cleanupTransitionEnd; constructor() { const ngZone = inject(NgZone); const renderer = inject(Renderer2); ngZone.runOutsideAngular(() => { this._cleanupTransitionEnd = renderer.listen(this._elementRef.nativeElement, 'transitionend', this._handleTransitionEnd); }); } activate() { const classList = this._elementRef.nativeElement.classList; classList.remove(DEACTIVATING_CLASS); classList.add(ACTIVATE_CLASS); } deactivate() { this._elementRef.nativeElement.classList.add(DEACTIVATING_CLASS); } _handleTransitionEnd = (event) => { const classList = this._elementRef.nativeElement.classList; const isDeactivating = classList.contains(DEACTIVATING_CLASS); if (event.propertyName === 'opacity' && isDeactivating) { classList.remove(ACTIVATE_CLASS, DEACTIVATING_CLASS); } }; ngOnDestroy() { this._cleanupTransitionEnd(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldLineRipple, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatFormFieldLineRipple, isStandalone: true, selector: "div[matFormFieldLineRipple]", host: { classAttribute: "mdc-line-ripple" }, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldLineRipple, decorators: [{ type: Directive, args: [{ selector: 'div[matFormFieldLineRipple]', host: { 'class': 'mdc-line-ripple', }, }] }], ctorParameters: () => [] }); /** * Internal component that creates an instance of the MDC notched-outline component. * * The component sets up the HTML structure and styles for the notched-outline. It provides * inputs to toggle the notch state and width. */ class MatFormFieldNotchedOutline { _elementRef = inject(ElementRef); _ngZone = inject(NgZone); /** Whether the notch should be opened. */ open = false; _notch; constructor() { } ngAfterViewInit() { const label = this._elementRef.nativeElement.querySelector('.mdc-floating-label'); if (label) { this._elementRef.nativeElement.classList.add('mdc-notched-outline--upgraded'); if (typeof requestAnimationFrame === 'function') { label.style.transitionDuration = '0s'; this._ngZone.runOutsideAngular(() => { requestAnimationFrame(() => (label.style.transitionDuration = '')); }); } } else { this._elementRef.nativeElement.classList.add('mdc-notched-outline--no-label'); } } _setNotchWidth(labelWidth) { if (!this.open || !labelWidth) { this._notch.nativeElement.style.width = ''; } else { const NOTCH_ELEMENT_PADDING = 8; const NOTCH_ELEMENT_BORDER = 1; this._notch.nativeElement.style.width = `calc(${labelWidth}px * var(--mat-mdc-form-field-floating-label-scale, 0.75) + ${NOTCH_ELEMENT_PADDING + NOTCH_ELEMENT_BORDER}px)`; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldNotchedOutline, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.2.6", type: MatFormFieldNotchedOutline, isStandalone: true, selector: "div[matFormFieldNotchedOutline]", inputs: { open: ["matFormFieldNotchedOutlineOpen", "open"] }, host: { properties: { "class.mdc-notched-outline--notched": "open" }, classAttribute: "mdc-notched-outline" }, viewQueries: [{ propertyName: "_notch", first: true, predicate: ["notch"], descendants: true }], ngImport: i0, template: "<div class=\"mat-mdc-notch-piece mdc-notched-outline__leading\"></div>\n<div class=\"mat-mdc-notch-piece mdc-notched-outline__notch\" #notch>\n <ng-content></ng-content>\n</div>\n<div class=\"mat-mdc-notch-piece mdc-notched-outline__trailing\"></div>\n", changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldNotchedOutline, decorators: [{ type: Component, args: [{ selector: 'div[matFormFieldNotchedOutline]', host: { 'class': 'mdc-notched-outline', // Besides updating the notch state through the MDC component, we toggle this class through // a host binding in order to ensure that the notched-outline renders correctly on the server. '[class.mdc-notched-outline--notched]': 'open', }, changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"mat-mdc-notch-piece mdc-notched-outline__leading\"></div>\n<div class=\"mat-mdc-notch-piece mdc-notched-outline__notch\" #notch>\n <ng-content></ng-content>\n</div>\n<div class=\"mat-mdc-notch-piece mdc-notched-outline__trailing\"></div>\n" }] }], ctorParameters: () => [], propDecorators: { open: [{ type: Input, args: ['matFormFieldNotchedOutlineOpen'] }], _notch: [{ type: ViewChild, args: ['notch'] }] } }); /** An interface which allows a control to work inside of a `MatFormField`. */ class MatFormFieldControl { /** The value of the control. */ value; /** * Stream that emits whenever the state of the control changes such that the parent `MatFormField` * needs to run change detection. */ stateChanges; /** The element ID for this control. */ id; /** The placeholder for this control. */ placeholder; /** Gets the AbstractControlDirective for this control. */ ngControl; /** Whether the control is focused. */ focused; /** Whether the control is empty. */ empty; /** Whether the `MatFormField` label should try to float. */ shouldLabelFloat; /** Whether the control is required. */ required; /** Whether the control is disabled. */ disabled; /** Whether the control is in an error state. */ errorState; /** * An optional name for the control type that can be used to distinguish `mat-form-field` elements * based on their control type. The form field will add a class, * `mat-form-field-type-{{controlType}}` to its root element. */ controlType; /** * Whether the input is currently in an autofilled state. If property is not present on the * control it is assumed to be false. */ autofilled; /** * Value of `aria-describedby` that should be merged with the described-by ids * which are set by the form-field. */ userAriaDescribedBy; /** * Whether to automatically assign the ID of the form field as the `for` attribute * on the `<label>` inside the form field. Set this to true to prevent the form * field from associating the label with non-native elements. */ disableAutomaticLabeling; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldControl, deps: [], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.6", type: MatFormFieldControl, isStandalone: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormFieldControl, decorators: [{ type: Directive }] }); /** @docs-private */ function getMatFormFieldPlaceholderConflictError() { return Error('Placeholder attribute and child element were both specified.'); } /** @docs-private */ function getMatFormFieldDuplicatedHintError(align) { return Error(`A hint was already declared for 'align="${align}"'.`); } /** @docs-private */ function getMatFormFieldMissingControlError() { return Error('mat-form-field must contain a MatFormFieldControl.'); } /** * Injection token that can be used to inject an instances of `MatFormField`. It serves * as alternative token to the actual `MatFormField` class which would cause unnecessary * retention of the `MatFormField` class and its component metadata. */ const MAT_FORM_FIELD = new InjectionToken('MatFormField'); /** * Injection token that can be used to configure the * default options for all form field within an app. */ const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken('MAT_FORM_FIELD_DEFAULT_OPTIONS'); /** Default appearance used by the form field. */ const DEFAULT_APPEARANCE = 'fill'; /** * Whether the label for form fields should by default float `always`, * `never`, or `auto`. */ const DEFAULT_FLOAT_LABEL = 'auto'; /** Default way that the subscript element height is set. */ const DEFAULT_SUBSCRIPT_SIZING = 'fixed'; /** * Default transform for docked floating labels in a MDC text-field. This value has been * extracted from the MDC text-field styles because we programmatically modify the docked * label transform, but do not want to accidentally discard the default label transform. */ const FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM = `translateY(-50%)`; /** Container for form controls that applies Material Design styling and behavior. */ class MatFormField { _elementRef = inject(ElementRef); _changeDetectorRef = inject(ChangeDetectorRef); _dir = inject(Directionality); _platform = inject(Platform); _idGenerator = inject(_IdGenerator); _ngZone = inject(NgZone); _injector = inject(Injector); _defaults = inject(MAT_FORM_FIELD_DEFAULT_OPTIONS, { optional: true, }); _textField; _iconPrefixContainer; _textPrefixContainer; _iconSuffixContainer; _textSuffixContainer; _floatingLabel; _notchedOutline; _lineRipple; _formFieldControl; _prefixChildren; _suffixChildren; _errorChildren; _hintChildren; _labelChild = contentChild(MatLabel); /** Whether the required marker should be hidden. */ get hideRequiredMarker() { return this._hideRequiredMarker; } set hideRequiredMarker(value) { this._hideRequiredMarker = coerceBooleanProperty(value); } _hideRequiredMarker = false; /** * Theme color of the form field. This API is supported in M2 themes only, it * has no effect in M3 themes. For color customization in M3, see https://material.angular.io/components/form-field/styling. * * For information on applying color variants in M3, see * https://material.angular.io/guide/material-2-theming#optional-add-backwards-compatibility-styles-for-color-variants */ color = 'primary'; /** Whether the label should always float or float as the user types. */ get floatLabel() { return this._floatLabel || this._defaults?.floatLabel || DEFAULT_FLOAT_LABEL; } set floatLabel(value) { if (value !== this._floatLabel) { this._floatLabel = value; // For backwards compatibility. Custom form field controls or directives might set // the "floatLabel" input and expect the form field view to be updated automatically. // e.g. autocomplete trigger. Ideally we'd get rid of this and the consumers would just // emit the "stateChanges" observable. TODO(devversion): consider removing. this._changeDetectorRef.markForCheck(); } } _floatLabel; /** The form field appearance style. */ get appearance() { return this._appearance; } set appearance(value) { const oldValue = this._appearance; const newAppearance = value || this._defaults?.appearance || DEFAULT_APPEARANCE; if (typeof ngDevMode === 'undefined' || ngDevMode) { if (newAppearance !== 'fill' && newAppearance !== 'outline') { throw new Error(`MatFormField: Invalid appearance "${newAppearance}", valid values are "fill" or "outline".`); } } this._appearance = newAppearance; if (this._appearance === 'outline' && this._appearance !== oldValue) { // If the appearance has been switched to `outline`, the label offset needs to be updated. // The update can happen once the view has been re-checked, but not immediately because // the view has not been updated and the notched-outline floating label is not present. this._needsOutlineLabelOffsetUpdate = true; } } _appearance = DEFAULT_APPEARANCE; /** * Whether the form field should reserve space for one line of hint/error text (default) * or to have the spacing grow from 0px as needed based on the size of the hint/error content. * Note that when using dynamic sizing, layout shifts will occur when hint/error text changes. */ get subscriptSizing() { return this._subscriptSizing || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING; } set subscriptSizing(value) { this._subscriptSizing = value || this._defaults?.subscriptSizing || DEFAULT_SUBSCRIPT_SIZING; } _subscriptSizing = null; /** Text for the form field hint. */ get hintLabel() { return this._hintLabel; } set hintLabel(value) { this._hintLabel = value; this._processHints(); } _hintLabel = ''; _hasIconPrefix = false; _hasTextPrefix = false; _hasIconSuffix = false; _hasTextSuffix = false; // Unique id for the internal form field label. _labelId = this._idGenerator.getId('mat-mdc-form-field-label-'); // Unique id for the hint label. _hintLabelId = this._idGenerator.getId('mat-mdc-hint-'); /** Gets the current form field control */ get _control() { return this._explicitFormFieldControl || this._formFieldControl; } set _control(value) { this._explicitFormFieldControl = value; } _destroyed = new Subject(); _isFocused = null; _explicitFormFieldControl; _needsOutlineLabelOffsetUpdate = false; _previousControl = null; _previousControlValidatorFn = null; _stateChanges; _valueChanges; _describedByChanges; _animationsDisabled; constructor() { const defaults = this._defaults; if (defaults) { if (defaults.appearance) { this.appearance = defaults.appearance; } this._hideRequiredMarker = Boolean(defaults?.hideRequiredMarker); if (defaults.color) { this.color = defaults.color; } } this._animationsDisabled = inject(ANIMATION_MODULE_TYPE, { optional: true }) === 'NoopAnimations'; } ngAfterViewInit() { // Initial focus state sync. This happens rarely, but we want to account for // it in case the form field control has "focused" set to true on init. this._updateFocusState(); if (!this._animationsDisabled) { this._ngZone.runOutsideAngular(() => { // Enable animations after a certain amount of time so that they don't run on init. setTimeout(() => { this._elementRef.nativeElement.classList.add('mat-form-field-animations-enabled'); }, 300); }); } // Because the above changes a value used in the template after it was checked, we need // to trigger CD or the change might not be reflected if there is no other CD scheduled. this._changeDetectorRef.detectChanges(); } ngAfterContentInit() { this._assertFormFieldControl(); this._initializeSubscript(); this._initializePrefixAndSuffix(); this._initializeOutlineLabelOffsetSubscriptions(); } ngAfterContentChecked() { this._assertFormFieldControl(); // if form field was being used with an input in first place and then replaced by other // component such as select. if (this._control !== this._previousControl) { this._initializeControl(this._previousControl); // keep a reference for last validator we had. if (this._control.ngControl && this._control.ngControl.control) { this._previousControlValidatorFn = this._control.ngControl.control.validator; } this._previousControl = this._control; } // make sure the the control has been initialized. if (this._control.ngControl && this._control.ngControl.control) { // get the validators for current control. const validatorFn = this._control.ngControl.control.validator; // if our current validatorFn isn't equal to it might be we are CD behind, marking the // component will allow us to catch up. if (validatorFn !== this._previousControlValidatorFn) { this._changeDetectorRef.markForCheck(); } } } ngOnDestroy() { this._stateChanges?.unsubscribe(); this._valueChanges?.unsubscribe(); this._describedByChanges?.unsubscribe(); this._destroyed.next(); this._destroyed.complete(); } /** * Gets the id of the label element. If no label is present, returns `null`. */ getLabelId = computed(() => (this._hasFloatingLabel() ? this._labelId : null)); /** * Gets an ElementRef for the element that a overlay attached to the form field * should be positioned relative to. */ getConnectedOverlayOrigin() { return this._textField || this._elementRef; } /** Animates the placeholder up and locks it in position. */ _animateAndLockLabel() { // This is for backwards compatibility only. Consumers of the form field might use // this method. e.g. the autocomplete trigger. This method has been added to the non-MDC // form field because setting "floatLabel" to "always" caused the label to float without // animation. This is different in MDC where the label always animates, so this method // is no longer necessary. There doesn't seem any benefit in adding logic to allow changing // the floating label state without animations. The non-MDC implementation was inconsistent // because it always animates if "floatLabel" is set away from "always". // TODO(devversion): consider removing this method when releasing the MDC form field. if (this._hasFloatingLabel()) { this.floatLabel = 'always'; } } /** Initializes the registered form field control. */ _initializeControl(previousControl) { const control = this._control; const classPrefix = 'mat-mdc-form-field-type-'; if (previousControl) { this._elementRef.nativeElement.classList.remove(classPrefix + previousControl.controlType); } if (control.controlType) { this._elementRef.nativeElement.classList.add(classPrefix + control.controlType); } // Subscribe to changes in the child control state in order to update the form field UI. this._stateChanges?.unsubscribe(); this._stateChanges = control.stateChanges.subscribe(() => { this._updateFocusState(); this._changeDetectorRef.markForCheck(); }); // Updating the `aria-describedby` touches the DOM. Only do it if it actually needs to change. this._describedByChanges?.unsubscribe(); this._describedByChanges = control.stateChanges .pipe(startWith([undefined, undefined]), map(() => [control.errorState, control.userAriaDescribedBy]), pairwise(), filter(([[prevErrorState, prevDescribedBy], [currentErrorState, currentDescribedBy]]) => { return prevErrorState !== currentErrorState || prevDescribedBy !== currentDescribedBy; })) .subscribe(() => this._syncDescribedByIds()); this._valueChanges?.unsubscribe(); // Run change detection if the value changes. if (control.ngControl && control.ngControl.valueChanges) { this._valueChanges = control.ngControl.valueChanges .pipe(takeUntil(this._destroyed)) .subscribe(() => this._changeDetectorRef.markForCheck()); } } _checkPrefixAndSuffixTypes() { this._hasIconPrefix = !!this._prefixChildren.find(p => !p._isText); this._hasTextPrefix = !!this._prefixChildren.find(p => p._isText); this._hasIconSuffix = !!this._suffixChildren.find(s => !s._isText); this._hasTextSuffix = !!this._suffixChildren.find(s => s._isText); } /** Initializes the prefix and suffix containers. */ _initializePrefixAndSuffix() { this._checkPrefixAndSuffixTypes(); // Mark the form field as dirty whenever the prefix or suffix children change. This // is necessary because we conditionally display the prefix/suffix containers based // on whether there is projected content. merge(this._prefixChildren.changes, this._suffixChildren.changes).subscribe(() => { this._checkPrefixAndSuffixTypes(); this._changeDetectorRef.markForCheck(); }); } /** * Initializes the subscript by validating hints and synchronizing "aria-describedby" ids * with the custom form field control. Also subscribes to hint and error changes in order * to be able to validate and synchronize ids on change. */ _initializeSubscript() { // Re-validate when the number of hints changes. this._hintChildren.changes.subscribe(() => { this._processHints(); this._changeDetectorRef.markForCheck(); }); // Update the aria-described by when the number of errors changes. this._errorChildren.changes.subscribe(() => { this._syncDescribedByIds(); this._changeDetectorRef.markForCheck(); }); // Initial mat-hint validation and subscript describedByIds sync. this._validateHints(); this._syncDescribedByIds(); } /** Throws an error if the form field's control is missing. */ _assertFormFieldControl() { if (!this._control && (typeof ngDevMode === 'undefined' || ngDevMode)) { throw getMatFormFieldMissingControlError(); } } _updateFocusState() { // Usually the MDC foundation would call "activateFocus" and "deactivateFocus" whenever // certain DOM events are emitted. This is not possible in our implementation of the // form field because we support abstract form field controls which are not necessarily // of type input, nor do we have a reference to a native form field control element. Instead // we handle the focus by checking if the abstract form field control focused state changes. if (this._control.focused && !this._isFocused) { this._isFocused = true; this._lineRipple?.activate(); } else if (!this._control.focused && (this._isFocused || this._isFocused === null)) { this._isFocused = false; this._lineRipple?.deactivate(); } this._textField?.nativeElement.classList.toggle('mdc-text-field--focused', this._control.focused); } /** * The floating label in the docked state needs to account for prefixes. The horizontal offset * is calculated whenever the appearance changes to `outline`, the prefixes change, or when the * form field is added to the DOM. This method sets up all subscriptions which are needed to * trigger the label offset update. */ _initializeOutlineLabelOffsetSubscriptions() { // Whenever the prefix changes, schedule an update of the label offset. // TODO(mmalerba): Use ResizeObserver to better support dynamically changing prefix content. this._prefixChildren.changes.subscribe(() => (this._needsOutlineLabelOffsetUpdate = true)); // TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and // `Write` phases. afterRender(() => { if (this._needsOutlineLabelOffsetUpdate) { this._needsOutlineLabelOffsetUpdate = false; this._updateOutlineLabelOffset(); } }, { injector: this._injector, }); this._dir.change .pipe(takeUntil(this._destroyed)) .subscribe(() => (this._needsOutlineLabelOffsetUpdate = true)); } /** Whether the floating label should always float or not. */ _shouldAlwaysFloat() { return this.floatLabel === 'always'; } _hasOutline() { return this.appearance === 'outline'; } /** * Whether the label should display in the infix. Labels in the outline appearance are * displayed as part of the notched-outline and are horizontally offset to account for * form field prefix content. This won't work in server side rendering since we cannot * measure the width of the prefix container. To make the docked label appear as if the * right offset has been calculated, we forcibly render the label inside the infix. Since * the label is part of the infix, the label cannot overflow the prefix content. */ _forceDisplayInfixLabel() { return !this._platform.isBrowser && this._prefixChildren.length && !this._shouldLabelFloat(); } _hasFloatingLabel = computed(() => !!this._labelChild()); _shouldLabelFloat() { if (!this._hasFloatingLabel()) { return false; } return this._control.shouldLabelFloat || this._shouldAlwaysFloat(); } /** * Determines whether a class from the AbstractControlDirective * should be forwarded to the host element. */ _shouldForward(prop) { const control = this._control ? this._control.ngControl : null; return control && control[prop]; } /** Gets the type of subscript message to render (error or hint). */ _getSubscriptMessageType() { return this._errorChildren && this._errorChildren.length > 0 && this._control.errorState ? 'error' : 'hint'; } /** Handle label resize events. */ _handleLabelResized() { this._refreshOutlineNotchWidth(); } /** Refreshes the width of the outline-notch, if present. */ _refreshOutlineNotchWidth() { if (!this._hasOutline() || !this._floatingLabel || !this._shouldLabelFloat()) { this._notchedOutline?._setNotchWidth(0); } else { this._notchedOutline?._setNotchWidth(this._floatingLabel.getWidth()); } } /** Does any extra processing that is required when handling the hints. */ _processHints() { this._validateHints(); this._syncDescribedByIds(); } /** * Ensure that there is a maximum of one of each "mat-hint" alignment specified. The hint * label specified set through the input is being considered as "start" aligned. * * This method is a noop if Angular runs in production mode. */ _validateHints() { if (this._hintChildren && (typeof ngDevMode === 'undefined' || ngDevMode)) { let startHint; let endHint; this._hintChildren.forEach((hint) => { if (hint.align === 'start') { if (startHint || this.hintLabel) { throw getMatFormFieldDuplicatedHintError('start'); } startHint = hint; } else if (hint.align === 'end') { if (endHint) { throw getMatFormFieldDuplicatedHintError('end'); } endHint = hint; } }); } } /** * Sets the list of element IDs that describe the child control. This allows the control to update * its `aria-describedby` attribute accordingly. */ _syncDescribedByIds() { if (this._control) { let ids = []; // TODO(wagnermaciel): Remove the type check when we find the root cause of this bug. if (this._control.userAriaDescribedBy && typeof this._control.userAriaDescribedBy === 'string') { ids.push(...this._control.userAriaDescribedBy.split(' ')); } if (this._getSubscriptMessageType() === 'hint') { const startHint = this._hintChildren ? this._hintChildren.find(hint => hint.align === 'start') : null; const endHint = this._hintChildren ? this._hintChildren.find(hint => hint.align === 'end') : null; if (startHint) { ids.push(startHint.id); } else if (this._hintLabel) { ids.push(this._hintLabelId); } if (endHint) { ids.push(endHint.id); } } else if (this._errorChildren) { ids.push(...this._errorChildren.map(error => error.id)); } this._control.setDescribedByIds(ids); } } /** * Updates the horizontal offset of the label in the outline appearance. In the outline * appearance, the notched-outline and label are not relative to the infix container because * the outline intends to surround prefixes, suffixes and the infix. This means that the * floating label by default overlaps prefixes in the docked state. To avoid this, we need to * horizontally offset the label by the width of the prefix container. The MDC text-field does * not need to do this because they use a fixed width for prefixes. Hence, they can simply * incorporate the horizontal offset into their default text-field styles. */ _updateOutlineLabelOffset() { if (!this._hasOutline() || !this._floatingLabel) { return; } const floatingLabel = this._floatingLabel.element; // If no prefix is displayed, reset the outline label offset from potential // previous label offset updates. if (!(this._iconPrefixContainer || this._textPrefixContainer)) { floatingLabel.style.transform = ''; return; } // If the form field is not attached to the DOM yet (e.g. in a tab), we defer // the label offset update until the zone stabilizes. if (!this._isAttachedToDom()) { this._needsOutlineLabelOffsetUpdate = true; return; } const iconPrefixContainer = this._iconPrefixContainer?.nativeElement; const textPrefixContainer = this._textPrefixContainer?.nativeElement; const iconSuffixContainer = this._iconSuffixContainer?.nativeElement; const textSuffixContainer = this._textSuffixContainer?.nativeElement; const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0; const textPrefixContainerWidth = textPrefixContainer?.getBoundingClientRect().width ?? 0; const iconSuffixContainerWidth = iconSuffixContainer?.getBoundingClientRect().width ?? 0; const textSuffixContainerWidth = textSuffixContainer?.getBoundingClientRect().width ?? 0; // If the directionality is RTL, the x-axis transform needs to be inverted. This // is because `transformX` does not change based on the page directionality. const negate = this._dir.value === 'rtl' ? '-1' : '1'; const prefixWidth = `${iconPrefixContainerWidth + textPrefixContainerWidth}px`; const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)`; const labelHorizontalOffset = `calc(${negate} * (${prefixWidth} + ${labelOffset}))`; // Update the translateX of the floating label to account for the prefix container, // but allow the CSS to override this setting via a CSS variable when the label is // floating. floatingLabel.style.transform = `var( --mat-mdc-form-field-label-transform, ${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset}) )`; // Prevent the label from overlapping the suffix when in resting position. const prefixAndSuffixWidth = iconPrefixContainerWidth + textPrefixContainerWidth + iconSuffixContainerWidth + textSuffixContainerWidth; this._elementRef.nativeElement.style.setProperty('--mat-form-field-notch-max-width', `calc(100% - ${prefixAndSuffixWidth}px)`); } /** Checks whether the form field is attached to the DOM. */ _isAttachedToDom() { const element = this._elementRef.nativeElement; if (element.getRootNode) { const rootNode = element.getRootNode(); // If the element is inside the DOM the root node will be either the document // or the closest shadow root, otherwise it'll be the element itself. return rootNode && rootNode !== element; } // Otherwise fall back to checking if it's in the document. This doesn't account for // shadow DOM, however browser that support shadow DOM should support `getRootNode` as well. return document.documentElement.contains(element); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.6", ngImport: i0, type: MatFormField, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.6", type: MatFormField, isStandalone: true, selector: "mat-form-field", inputs: { hideRequiredMarker: "hideRequiredMarker", color: "color", floatLabel: "floatLabel", appearance: "appearance", subscriptSizing: "subscriptSizing", hintLabel: "hintLabel" }, host: { properties: { "class.mat-mdc-form-field-label-always-float": "_shouldAlwaysFloat()", "class.mat-mdc-form-field-has-icon-prefix": "_hasIconPrefix", "class.mat-mdc-form-field-has-icon-suffix": "_hasIconSuffix", "class.mat-form-field-invalid": "_control.errorState", "class.mat-form-field-disabled": "_control.disabled", "class.mat-form-field-autofilled": "_control.autofilled", "class.mat-form-field-appearance-fill": "appearance == \"fill\"", "class.mat-form-field-appearance-outline": "appearance == \"outline\"", "class.mat-form-field-hide-placeholder": "_hasFloatingLabel() && !_shouldLabelFloat()", "class.mat-focused": "_control.focused", "class.mat-primary": "color !== \"accent\" && color !== \"warn\"", "class.mat-accent": "color === \"accent\"", "class.mat-warn": "color === \"warn\"", "class.ng-untouched": "_shouldForward(\"untouched\")", "class.ng-touched": "_shouldForward(\"touched\")", "class.ng-pristine": "_shouldForward(\"pristine\")", "class.ng-dirty": "_shouldForward(\"dirty\")", "class.ng-valid": "_shouldForward(\"valid\")", "class.ng-invalid": "_shouldForward(\"invalid\")", "class.ng-pending": "_shouldForward(\"pending\")" }, classAttribute: "mat-mdc-form-field" }, providers: [ { provide: MAT_FORM_FIELD, useExisting: MatFormField }, { provide: FLOATING_LABEL_PARENT, useExisting: MatFormField }, ], queries: [{ propertyName: "_labelChild", first: true, predicate: MatLabel, descendants: true, isSignal: true }, { propertyName: "_formFieldControl", first: true, predicate: MatFormFieldControl, descendants: true }, { propertyName: "_prefixChildren", predicate: MAT_PREFIX, descendants: true }, { propertyName: "_suffixChildren", predicate: MAT_SUFFIX, descendants: true }, { propertyName: "_errorChildren", predicate: MAT_ERROR, descendants: true }, { propertyName: "_hintChildren", predicate: MatHint, descendants: true }], viewQueries: [{ propertyName: "_te