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' ;
@@ -27,16 +28,16 @@ import {
2728 QueryList ,
2829 ViewChild ,
2930 ViewEncapsulation ,
30- afterRender ,
31+ afterRenderEffect ,
3132 computed ,
3233 contentChild ,
3334 inject ,
35+ signal ,
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 ,
@@ -250,10 +251,9 @@ export class MatFormField
250251 /** The form field appearance style. */
251252 @Input ( )
252253 get appearance ( ) : MatFormFieldAppearance {
253- return this . _appearance ;
254+ return this . _appearanceSignal ( ) ;
254255 }
255256 set appearance ( value : MatFormFieldAppearance ) {
256- const oldValue = this . _appearance ;
257257 const newAppearance = value || this . _defaults ?. appearance || DEFAULT_APPEARANCE ;
258258 if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
259259 if ( newAppearance !== 'fill' && newAppearance !== 'outline' ) {
@@ -262,15 +262,9 @@ export class MatFormField
262262 ) ;
263263 }
264264 }
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- }
265+ this . _appearanceSignal . set ( newAppearance ) ;
272266 }
273- private _appearance : MatFormFieldAppearance = DEFAULT_APPEARANCE ;
267+ private _appearanceSignal = signal ( DEFAULT_APPEARANCE ) ;
274268
275269 /**
276270 * Whether the form field should reserve space for one line of hint/error text (default)
@@ -319,7 +313,6 @@ export class MatFormField
319313 private _destroyed = new Subject < void > ( ) ;
320314 private _isFocused : boolean | null = null ;
321315 private _explicitFormFieldControl : MatFormFieldControl < any > ;
322- private _needsOutlineLabelOffsetUpdate = false ;
323316 private _previousControl : MatFormFieldControl < unknown > | null = null ;
324317 private _previousControlValidatorFn : ValidatorFn | null = null ;
325318 private _stateChanges : Subscription | undefined ;
@@ -399,6 +392,7 @@ export class MatFormField
399392 }
400393
401394 ngOnDestroy ( ) {
395+ this . _outlineLabelOffsetResizeObserver ?. disconnect ( ) ;
402396 this . _stateChanges ?. unsubscribe ( ) ;
403397 this . _valueChanges ?. unsubscribe ( ) ;
404398 this . _describedByChanges ?. unsubscribe ( ) ;
@@ -546,34 +540,48 @@ export class MatFormField
546540 ) ;
547541 }
548542
543+ private _outlineLabelOffsetResizeObserver = globalThis . ResizeObserver
544+ ? new globalThis . ResizeObserver ( ( ) => this . _updateOutlineLabelOffset ( ) )
545+ : null ;
546+
549547 /**
550548 * The floating label in the docked state needs to account for prefixes. The horizontal offset
551549 * is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
552550 * form field is added to the DOM. This method sets up all subscriptions which are needed to
553551 * trigger the label offset update.
554552 */
555553 private _initializeOutlineLabelOffsetSubscriptions ( ) {
554+ if ( ! this . _injector . get ( Platform ) . isBrowser ) {
555+ return ;
556+ }
556557 // 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-
560558 // TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
561559 // `Write` phases.
562- afterRender (
560+ afterRenderEffect (
563561 ( ) => {
564- if ( this . _needsOutlineLabelOffsetUpdate ) {
565- this . _needsOutlineLabelOffsetUpdate = false ;
562+ if ( this . _appearanceSignal ( ) === 'outline' ) {
563+ // Trigger effect on directionality changes.
564+ this . _dir . valueSignal ( ) ;
565+ const prefixSuffixEls = [
566+ this . _textPrefixContainer ,
567+ this . _iconPrefixContainer ,
568+ this . _textSuffixContainer ,
569+ this . _iconSuffixContainer ,
570+ ]
571+ . filter ( e => e != null )
572+ . map ( e => e . nativeElement ) ;
573+ for ( const el of prefixSuffixEls ) {
574+ this . _outlineLabelOffsetResizeObserver ?. observe ( el , { box : 'border-box' } ) ;
575+ }
566576 this . _updateOutlineLabelOffset ( ) ;
577+ } else {
578+ this . _outlineLabelOffsetResizeObserver ?. disconnect ( ) ;
567579 }
568580 } ,
569581 {
570582 injector : this . _injector ,
571583 } ,
572584 ) ;
573-
574- this . _dir . change
575- . pipe ( takeUntil ( this . _destroyed ) )
576- . subscribe ( ( ) => ( this . _needsOutlineLabelOffsetUpdate = true ) ) ;
577585 }
578586
579587 /** Whether the floating label should always float or not. */
@@ -732,7 +740,6 @@ export class MatFormField
732740 // If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733741 // the label offset update until the zone stabilizes.
734742 if ( ! this . _isAttachedToDom ( ) ) {
735- this . _needsOutlineLabelOffsetUpdate = true ;
736743 return ;
737744 }
738745 const iconPrefixContainer = this . _iconPrefixContainer ?. nativeElement ;
0 commit comments