55 * Use of this source code is governed by an MIT-style license that can be
66 * found in the LICENSE file at https://angular.dev/license
77 */
8+ import { _IdGenerator } from '@angular/cdk/a11y' ;
89import { Directionality } from '@angular/cdk/bidi' ;
910import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ;
1011import { Platform } from '@angular/cdk/platform' ;
@@ -20,23 +21,23 @@ import {
2021 ContentChildren ,
2122 ElementRef ,
2223 InjectionToken ,
23- Injector ,
2424 Input ,
2525 NgZone ,
2626 OnDestroy ,
2727 QueryList ,
2828 ViewChild ,
2929 ViewEncapsulation ,
30- afterRender ,
30+ afterRenderEffect ,
3131 computed ,
3232 contentChild ,
3333 inject ,
34+ signal ,
35+ viewChild ,
3436} from '@angular/core' ;
3537import { AbstractControlDirective , ValidatorFn } from '@angular/forms' ;
36- import { _animationsDisabled , ThemePalette } from '../core' ;
37- import { _IdGenerator } from '@angular/cdk/a11y' ;
3838import { Subject , Subscription , merge } from 'rxjs' ;
39- import { map , pairwise , takeUntil , filter , startWith } from 'rxjs/operators' ;
39+ import { filter , map , pairwise , startWith , takeUntil } from 'rxjs/operators' ;
40+ import { ThemePalette , _animationsDisabled } from '../core' ;
4041import { MAT_ERROR , MatError } from './directives/error' ;
4142import {
4243 FLOATING_LABEL_PARENT ,
@@ -189,7 +190,6 @@ export class MatFormField
189190 private _platform = inject ( Platform ) ;
190191 private _idGenerator = inject ( _IdGenerator ) ;
191192 private _ngZone = inject ( NgZone ) ;
192- private _injector = inject ( Injector ) ;
193193 private _defaults = inject < MatFormFieldDefaultOptions > ( MAT_FORM_FIELD_DEFAULT_OPTIONS , {
194194 optional : true ,
195195 } ) ;
@@ -203,6 +203,21 @@ export class MatFormField
203203 @ViewChild ( MatFormFieldNotchedOutline ) _notchedOutline : MatFormFieldNotchedOutline | undefined ;
204204 @ViewChild ( MatFormFieldLineRipple ) _lineRipple : MatFormFieldLineRipple | undefined ;
205205
206+ private _iconPrefixContainerSignal = viewChild < ElementRef < HTMLElement > > ( 'iconPrefixContainer' ) ;
207+ private _textPrefixContainerSignal = viewChild < ElementRef < HTMLElement > > ( 'textPrefixContainer' ) ;
208+ private _iconSuffixContainerSignal = viewChild < ElementRef < HTMLElement > > ( 'iconSuffixContainer' ) ;
209+ private _textSuffixContainerSignal = viewChild < ElementRef < HTMLElement > > ( 'textSuffixContainer' ) ;
210+ private _prefixSuffixContainers = computed ( ( ) => {
211+ return [
212+ this . _iconPrefixContainerSignal ( ) ,
213+ this . _textPrefixContainerSignal ( ) ,
214+ this . _iconSuffixContainerSignal ( ) ,
215+ this . _textSuffixContainerSignal ( ) ,
216+ ]
217+ . map ( container => container ?. nativeElement )
218+ . filter ( e => e !== undefined ) ;
219+ } ) ;
220+
206221 @ContentChild ( _MatFormFieldControl ) _formFieldControl : MatFormFieldControl < any > ;
207222 @ContentChildren ( MAT_PREFIX , { descendants : true } ) _prefixChildren : QueryList < MatPrefix > ;
208223 @ContentChildren ( MAT_SUFFIX , { descendants : true } ) _suffixChildren : QueryList < MatSuffix > ;
@@ -250,10 +265,9 @@ export class MatFormField
250265 /** The form field appearance style. */
251266 @Input ( )
252267 get appearance ( ) : MatFormFieldAppearance {
253- return this . _appearance ;
268+ return this . _appearanceSignal ( ) ;
254269 }
255270 set appearance ( value : MatFormFieldAppearance ) {
256- const oldValue = this . _appearance ;
257271 const newAppearance = value || this . _defaults ?. appearance || DEFAULT_APPEARANCE ;
258272 if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
259273 if ( newAppearance !== 'fill' && newAppearance !== 'outline' ) {
@@ -262,15 +276,9 @@ export class MatFormField
262276 ) ;
263277 }
264278 }
265- this . _appearance = newAppearance ;
266- if ( this . _appearance === 'outline' && this . _appearance !== oldValue ) {
267- // If the appearance has been switched to `outline`, the label offset needs to be updated.
268- // The update can happen once the view has been re-checked, but not immediately because
269- // the view has not been updated and the notched-outline floating label is not present.
270- this . _needsOutlineLabelOffsetUpdate = true ;
271- }
279+ this . _appearanceSignal . set ( newAppearance ) ;
272280 }
273- private _appearance : MatFormFieldAppearance = DEFAULT_APPEARANCE ;
281+ private _appearanceSignal = signal ( DEFAULT_APPEARANCE ) ;
274282
275283 /**
276284 * Whether the form field should reserve space for one line of hint/error text (default)
@@ -319,7 +327,6 @@ export class MatFormField
319327 private _destroyed = new Subject < void > ( ) ;
320328 private _isFocused : boolean | null = null ;
321329 private _explicitFormFieldControl : MatFormFieldControl < any > ;
322- private _needsOutlineLabelOffsetUpdate = false ;
323330 private _previousControl : MatFormFieldControl < unknown > | null = null ;
324331 private _previousControlValidatorFn : ValidatorFn | null = null ;
325332 private _stateChanges : Subscription | undefined ;
@@ -341,6 +348,8 @@ export class MatFormField
341348 this . color = defaults . color ;
342349 }
343350 }
351+
352+ this . _syncOutlineLabelOffset ( ) ;
344353 }
345354
346355 ngAfterViewInit ( ) {
@@ -366,7 +375,6 @@ export class MatFormField
366375 this . _assertFormFieldControl ( ) ;
367376 this . _initializeSubscript ( ) ;
368377 this . _initializePrefixAndSuffix ( ) ;
369- this . _initializeOutlineLabelOffsetSubscriptions ( ) ;
370378 }
371379
372380 ngAfterContentChecked ( ) {
@@ -399,6 +407,7 @@ export class MatFormField
399407 }
400408
401409 ngOnDestroy ( ) {
410+ this . _outlineLabelOffsetResizeObserver ?. disconnect ( ) ;
402411 this . _stateChanges ?. unsubscribe ( ) ;
403412 this . _valueChanges ?. unsubscribe ( ) ;
404413 this . _describedByChanges ?. unsubscribe ( ) ;
@@ -546,34 +555,37 @@ export class MatFormField
546555 ) ;
547556 }
548557
558+ private _outlineLabelOffsetResizeObserver : ResizeObserver | null = null ;
559+
549560 /**
550561 * The floating label in the docked state needs to account for prefixes. The horizontal offset
551562 * is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
552563 * form field is added to the DOM. This method sets up all subscriptions which are needed to
553564 * trigger the label offset update.
554565 */
555- private _initializeOutlineLabelOffsetSubscriptions ( ) {
566+ private _syncOutlineLabelOffset ( ) {
556567 // Whenever the prefix changes, schedule an update of the label offset.
557- // TODO(mmalerba): Use ResizeObserver to better support dynamically changing prefix content.
558- this . _prefixChildren . changes . subscribe ( ( ) => ( this . _needsOutlineLabelOffsetUpdate = true ) ) ;
559-
560568 // TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
561569 // `Write` phases.
562- afterRender (
563- ( ) => {
564- if ( this . _needsOutlineLabelOffsetUpdate ) {
565- this . _needsOutlineLabelOffsetUpdate = false ;
566- this . _updateOutlineLabelOffset ( ) ;
570+ afterRenderEffect ( ( ) => {
571+ if ( this . _appearanceSignal ( ) === 'outline' ) {
572+ this . _updateOutlineLabelOffset ( ) ;
573+ if ( ! globalThis . ResizeObserver ) {
574+ return ;
567575 }
568- } ,
569- {
570- injector : this . _injector ,
571- } ,
572- ) ;
573576
574- this . _dir . change
575- . pipe ( takeUntil ( this . _destroyed ) )
576- . subscribe ( ( ) => ( this . _needsOutlineLabelOffsetUpdate = true ) ) ;
577+ // Setup a resize observer to monitor changes to the size of the prefix / suffix and
578+ // readjust the label offset.
579+ this . _outlineLabelOffsetResizeObserver ||= new globalThis . ResizeObserver ( ( ) =>
580+ this . _updateOutlineLabelOffset ( ) ,
581+ ) ;
582+ for ( const el of this . _prefixSuffixContainers ( ) ) {
583+ this . _outlineLabelOffsetResizeObserver . observe ( el , { box : 'border-box' } ) ;
584+ }
585+ } else {
586+ this . _outlineLabelOffsetResizeObserver ?. disconnect ( ) ;
587+ }
588+ } ) ;
577589 }
578590
579591 /** Whether the floating label should always float or not. */
@@ -719,6 +731,7 @@ export class MatFormField
719731 * incorporate the horizontal offset into their default text-field styles.
720732 */
721733 private _updateOutlineLabelOffset ( ) {
734+ const dir = this . _dir . valueSignal ( ) ;
722735 if ( ! this . _hasOutline ( ) || ! this . _floatingLabel ) {
723736 return ;
724737 }
@@ -732,7 +745,6 @@ export class MatFormField
732745 // If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733746 // the label offset update until the zone stabilizes.
734747 if ( ! this . _isAttachedToDom ( ) ) {
735- this . _needsOutlineLabelOffsetUpdate = true ;
736748 return ;
737749 }
738750 const iconPrefixContainer = this . _iconPrefixContainer ?. nativeElement ;
@@ -745,7 +757,7 @@ export class MatFormField
745757 const textSuffixContainerWidth = textSuffixContainer ?. getBoundingClientRect ( ) . width ?? 0 ;
746758 // If the directionality is RTL, the x-axis transform needs to be inverted. This
747759 // is because `transformX` does not change based on the page directionality.
748- const negate = this . _dir . value === 'rtl' ? '-1' : '1' ;
760+ const negate = dir === 'rtl' ? '-1' : '1' ;
749761 const prefixWidth = `${ iconPrefixContainerWidth + textPrefixContainerWidth } px` ;
750762 const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)` ;
751763 const labelHorizontalOffset = `calc(${ negate } * (${ prefixWidth } + ${ labelOffset } ))` ;
0 commit comments