license import { Directionality } from '@angular/cdk/bidi' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { AfterContentChecked , AfterContentInit , AfterViewInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChild , ContentChildren , ElementRef , Inject , InjectionToken , Input , NgZone , Optional , QueryList , ViewChild , ViewEncapsulation , OnDestroy , } from '@angular/core' ; import { CanColor , CanColorCtor , FloatLabelType , LabelOptions , MAT_LABEL_GLOBAL_OPTIONS , mixinColor , } from '@angular/material/core' ; import { fromEvent , merge , Subject } from 'rxjs' ; import { startWith , take , takeUntil } from 'rxjs/operators' ; import { MatError } from './error' ; import { matFormFieldAnimations } from './form-field-animations' ; import { MatFormFieldControl } from './form-field-control' ; import { getMatFormFieldDuplicatedHintError , getMatFormFieldMissingControlError , getMatFormFieldPlaceholderConflictError , } from './form-field-errors' ; import { MatHint } from './hint' ; import { MatLabel } from './label' ; import { MatPlaceholder } from './placeholder' ; import { MatPrefix } from './prefix' ; import { MatSuffix } from './suffix' ; import { Platform } from '@angular/cdk/platform' ; import { NgControl } from '@angular/forms' ; import { ANIMATION_MODULE_TYPE } from '@angular/platform-browser/animations' ; let nextUniqueId = 0 ; const floatingLabelScale = 0.75 ; const outlineGapPadding = 5 ; docs-private export class MatFormFieldBase { constructor ( public _elementRef ) { } } docs-private export const _MatFormFieldMixinBase = mixinColor ( MatFormFieldBase , 'primary' ) ; /** Possible appearance styles for the form field. */ export type MatFormFieldAppearance = 'legacy' | 'standard' | 'fill' | 'outline' ; /**
 * Represents the default options form the form field that can be configured
 * using the `MAT_FORM_FIELD_DEFAULT_OPTIONS` injection token.
 */ export interface MatFormFieldDefaultOptions { appearance ? : MatFormFieldAppearance ; } /**
 * Injection token that can be used to configure the
 * default options for all form field within an app.
 */ export const MAT_FORM_FIELD_DEFAULT_OPTIONS = new InjectionToken < MatFormFieldDefaultOptions > ( 'MAT_FORM_FIELD_DEFAULT_OPTIONS' ) ; /** Container for form controls that applies Material Design styling and behavior. */ @ Component ( { moduleId : module . id , selector : 'mat-form-field' , exportAs : 'matFormField' , templateUrl : 'form-field.html' , styleUrls : [ 'form-field.css' , 'form-field-fill.css' , 'form-field-input.css' , 'form-field-legacy.css' , 'form-field-outline.css' , 'form-field-standard.css' , ] , animations : [ matFormFieldAnimations . transitionMessages ] , host : { 'class' : 'mat-form-field' , '[class.mat-form-field-appearance-standard]' : 'appearance == "standard"' , '[class.mat-form-field-appearance-fill]' : 'appearance == "fill"' , '[class.mat-form-field-appearance-outline]' : 'appearance == "outline"' , '[class.mat-form-field-appearance-legacy]' : 'appearance == "legacy"' , '[class.mat-form-field-invalid]' : '_control.errorState' , '[class.mat-form-field-can-float]' : '_canLabelFloat' , '[class.mat-form-field-should-float]' : '_shouldLabelFloat()' , '[class.mat-form-field-hide-placeholder]' : '_hideControlPlaceholder()' , '[class.mat-form-field-disabled]' : '_control.disabled' , '[class.mat-form-field-autofilled]' : '_control.autofilled' , '[class.mat-focused]' : '_control.focused' , '[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")' , '[class._mat-animation-noopable]' : '!_animationsEnabled' , } , inputs : [ 'color' ] , encapsulation : ViewEncapsulation . None , changeDetection : ChangeDetectionStrategy . OnPush , } ) export class MatFormField extends _MatFormFieldMixinBase implements AfterContentInit , AfterContentChecked , AfterViewInit , OnDestroy , CanColor { private _labelOptions : LabelOptions ; /**
     * Whether the outline gap needs to be calculated
     * immediately on the next change detection run.
     */ private _outlineGapCalculationNeededImmediately = false ; /** Whether the outline gap needs to be calculated next time the zone has stabilized. */ private _outlineGapCalculationNeededOnStable = false ; private _destroyed = new Subject < void > ( ) ; /** The form-field appearance style. */ @ Input ( ) get appearance ( ) : MatFormFieldAppearance { return this . _appearance ; } set appearance ( value ) { const oldValue = this . _appearance ; this . _appearance = value || ( this . _defaults && this . _defaults . appearance ) || 'legacy' ; if ( this . _appearance === 'outline' && oldValue !== value ) { this . _updateOutlineGapOnStable ( ) ; } } _appearance : MatFormFieldAppearance ; /** Whether the required marker should be hidden. */ @ Input ( ) get hideRequiredMarker ( ) : boolean { return this . _hideRequiredMarker ; } set hideRequiredMarker ( value ) { this . _hideRequiredMarker = coerceBooleanProperty ( value ) ; } private _hideRequiredMarker : boolean ; /** Override for the logic that disables the label animation in certain cases. */ private _showAlwaysAnimate = false ; /** Whether the floating label should always float or not. */ get _shouldAlwaysFloat ( ) : boolean { return this . floatLabel === 'always' && ! this . _showAlwaysAnimate ; } /** Whether the label can float or not. */ get _canLabelFloat ( ) : boolean { return this . floatLabel !== 'never' ; } /** State of the mat-hint and mat-error animations. */ _subscriptAnimationState : string = '' ; /** Text for the form field hint. */ @ Input ( ) get hintLabel ( ) : string { return this . _hintLabel ; } set hintLabel ( value ) { this . _hintLabel = value ; this . _processHints ( ) ; } private _hintLabel = '' ; _hintLabelId : string = `mat-hint-${ nextUniqueId ++ }` ; _labelId = `mat-form-field-label-${ nextUniqueId ++ }` ; /**
     * Whether the label should always float, never float or float as the user types.
     *
     * Note: only the legacy appearance supports the `never` option. `never` was originally added as a
     * way to make the floating label emulate the behavior of a standard input placeholder. However
     * the form field now supports both floating labels and placeholders. Therefore in the non-legacy
     * appearances the `never` option has been disabled in favor of just using the placeholder.
     */ @ Input ( ) get floatLabel ( ) : FloatLabelType { return this . appearance !== 'legacy' && this . _floatLabel === 'never' ? 'auto' : this . _floatLabel ; } set floatLabel ( value ) { if ( value !== this . _floatLabel ) { this . _floatLabel = value || this . _labelOptions . float || 'auto' ; this . _changeDetectorRef . markForCheck ( ) ; } } private _floatLabel : FloatLabelType ; /** Whether the Angular animations are enabled. */ _animationsEnabled : boolean ; deprecated breaking-change @ ViewChild ( 'underline' ) underlineRef : ElementRef ; @ ViewChild ( 'connectionContainer' ) _connectionContainerRef : ElementRef ; @ ViewChild ( 'inputContainer' ) _inputContainerRef : ElementRef ; @ ViewChild ( 'label' ) private _label : ElementRef ; @ ContentChild ( MatFormFieldControl ) _control : MatFormFieldControl < any > ; @ ContentChild ( MatPlaceholder ) _placeholderChild : MatPlaceholder ; @ ContentChild ( MatLabel ) _labelChild : MatLabel ; @ ContentChildren ( MatError ) _errorChildren : QueryList < MatError > ; @ ContentChildren ( MatHint ) _hintChildren : QueryList < MatHint > ; @ ContentChildren ( MatPrefix ) _prefixChildren : QueryList < MatPrefix > ; @ ContentChildren ( MatSuffix ) _suffixChildren : QueryList < MatSuffix > ; constructor ( public _elementRef , private _changeDetectorRef , @ Optional ( ) @ Inject ( MAT_LABEL_GLOBAL_OPTIONS ) labelOptions , @ Optional ( ) private _dir , @ Optional ( ) @ Inject ( MAT_FORM_FIELD_DEFAULT_OPTIONS ) private _defaults , private _platform ? , private _ngZone ? , @ Optional ( ) @ Inject ( ANIMATION_MODULE_TYPE ) _animationMode ? ) { super ( _elementRef ) ; this . _labelOptions = labelOptions ? labelOptions : { } ; this . floatLabel = this . _labelOptions . float || 'auto' ; this . _animationsEnabled = _animationMode !== 'NoopAnimations' ; this . appearance = ( _defaults && _defaults . appearance ) ? _defaults . appearance : 'legacy' ; } /**
     * Gets an ElementRef for the element that a overlay attached to the form-field should be
     * positioned relative to.
     */ getConnectedOverlayOrigin ( ) { return this . _connectionContainerRef || this . _elementRef ; } ngAfterContentInit ( ) { this . _validateControlChild ( ) ; const control = this . _control ; if ( control . controlType ) { this . _elementRef . nativeElement . classList . add ( `mat-form-field-type-${ control . controlType }` ) ; } control . stateChanges . pipe ( startWith < void > ( null ! ) ) . subscribe ( ( ) => { this . _validatePlaceholders ( ) ; this . _syncDescribedByIds ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; if ( control . ngControl && control . ngControl . valueChanges ) { control . ngControl . valueChanges . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . _changeDetectorRef . markForCheck ( ) ) ; } if ( this . _ngZone ) { this . _ngZone . onStable . asObservable ( ) . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => { if ( this . _outlineGapCalculationNeededOnStable ) { this . updateOutlineGap ( ) ; } } ) ; } merge ( this . _prefixChildren . changes , this . _suffixChildren . changes ) . subscribe ( ( ) => { this . _updateOutlineGapOnStable ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; this . _hintChildren . changes . pipe ( startWith ( null ) ) . subscribe ( ( ) => { this . _processHints ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; this . _errorChildren . changes . pipe ( startWith ( null ) ) . subscribe ( ( ) => { this . _syncDescribedByIds ( ) ; this . _changeDetectorRef . markForCheck ( ) ; } ) ; if ( this . _dir ) { this . _dir . change . pipe ( takeUntil ( this . _destroyed ) ) . subscribe ( ( ) => this . updateOutlineGap ( ) ) ; } } ngAfterContentChecked ( ) { this . _validateControlChild ( ) ; if ( this . _outlineGapCalculationNeededImmediately ) { this . updateOutlineGap ( ) ; } } ngAfterViewInit ( ) { this . _subscriptAnimationState = 'enter' ; this . _changeDetectorRef . detectChanges ( ) ; } ngOnDestroy ( ) { this . _destroyed . next ( ) ; this . _destroyed . complete ( ) ; } /** Determines whether a class from the NgControl should be forwarded to the host element. */ _shouldForward ( prop ) { const ngControl = this . _control ? this . _control . ngControl : null ; return ngControl && ngControl [ prop ] ; } _hasPlaceholder ( ) { return ! ! ( this . _control && this . _control . placeholder || this . _placeholderChild ) ; } _hasLabel ( ) { return ! ! this . _labelChild ; } _shouldLabelFloat ( ) { return this . _canLabelFloat && ( this . _control . shouldLabelFloat || this . _shouldAlwaysFloat ) ; } _hideControlPlaceholder ( ) { return this . appearance === 'legacy' && ! this . _hasLabel ( ) || this . _hasLabel ( ) && ! this . _shouldLabelFloat ( ) ; } _hasFloatingLabel ( ) { return this . _hasLabel ( ) || this . appearance === 'legacy' && this . _hasPlaceholder ( ) ; } /** Determines whether to display hints or errors. */ _getDisplayedMessages ( ) { return ( this . _errorChildren && this . _errorChildren . length > 0 && this . _control . errorState ) ? 'error' : 'hint' ; } /** Animates the placeholder up and locks it in position. */ _animateAndLockLabel ( ) { if ( this . _hasFloatingLabel ( ) && this . _canLabelFloat ) { if ( this . _animationsEnabled ) { this . _showAlwaysAnimate = true ; fromEvent ( this . _label . nativeElement , 'transitionend' ) . pipe ( take ( 1 ) ) . subscribe ( ( ) => { this . _showAlwaysAnimate = false ; } ) ; } this . floatLabel = 'always' ; this . _changeDetectorRef . markForCheck ( ) ; } } /**
     * Ensure that there is only one placeholder (either `placeholder` attribute on the child control
     * or child element with the `mat-placeholder` directive).
     */ private _validatePlaceholders ( ) { if ( this . _control . placeholder && this . _placeholderChild ) { throw getMatFormFieldPlaceholderConflictError ( ) ; } } /** Does any extra processing that is required when handling the hints. */ private _processHints ( ) { this . _validateHints ( ) ; this . _syncDescribedByIds ( ) ; } /**
     * Ensure that there is a maximum of one of each `<mat-hint>` alignment specified, with the
     * attribute being considered as `align="start"`.
     */ private _validateHints ( ) { if ( this . _hintChildren ) { 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.
     */ private _syncDescribedByIds ( ) { if ( this . _control ) { let ids = [ ] ; if ( this . _getDisplayedMessages ( ) === '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 = this . _errorChildren . map ( error => error . id ) ; } this . _control . setDescribedByIds ( ids ) ; } } /** Throws an error if the form field's control is missing. */ protected _validateControlChild ( ) { if ( ! this . _control ) { throw getMatFormFieldMissingControlError ( ) ; } } /**
     * Updates the width and position of the gap in the outline. Only relevant for the outline
     * appearance.
     */ updateOutlineGap ( ) { const labelEl = this . _label ? this . _label . nativeElement : null ; if ( this . appearance !== 'outline' || ! labelEl || ! labelEl . children . length || ! labelEl . textContent . trim ( ) ) { return ; } if ( this . _platform && ! this . _platform . isBrowser ) { return ; } if ( ! document . documentElement ! . contains ( this . _elementRef . nativeElement ) ) { this . _outlineGapCalculationNeededImmediately = true ; return ; } let startWidth = 0 ; let gapWidth = 0 ; const container = this . _connectionContainerRef . nativeElement ; const startEls = container . querySelectorAll ( '.mat-form-field-outline-start' ) ; const gapEls = container . querySelectorAll ( '.mat-form-field-outline-gap' ) ; if ( this . _label && this . _label . nativeElement . children . length ) { const containerRect = container . getBoundingClientRect ( ) ; if ( containerRect . width === 0 && containerRect . height === 0 ) { this . _outlineGapCalculationNeededOnStable = true ; this . _outlineGapCalculationNeededImmediately = false ; return ; } const containerStart = this . _getStartEnd ( containerRect ) ; const labelStart = this . _getStartEnd ( labelEl . children [ 0 ] . getBoundingClientRect ( ) ) ; let labelWidth = 0 ; for ( const child of labelEl . children ) { labelWidth += child . offsetWidth ; } startWidth = labelStart - containerStart - outlineGapPadding ; gapWidth = labelWidth > 0 ? labelWidth * floatingLabelScale + outlineGapPadding * 2 : 0 ; } for ( let i = 0 ; i < startEls . length ; i ++ ) { startEls . item ( i ) . style . width = `${ startWidth }px` ; } for ( let i = 0 ; i < gapEls . length ; i ++ ) { gapEls . item ( i ) . style . width = `${ gapWidth }px` ; } this . _outlineGapCalculationNeededOnStable = this . _outlineGapCalculationNeededImmediately = false ; } /** Gets the start end of the rect considering the current directionality. */ private _getStartEnd ( rect ) { return this . _dir && this . _dir . value === 'rtl' ? rect . right : rect . left ; } breaking-change private _updateOutlineGapOnStable ( ) { if ( this . _ngZone ) { this . _outlineGapCalculationNeededOnStable = true ; } else { Promise . resolve ( ) . then ( ( ) => this . updateOutlineGap ( ) ) ; } } }