@angular/material
Version:
Angular Material
974 lines (963 loc) • 135 kB
JavaScript
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