5
5
* Use of this source code is governed by an MIT-style license that can be
6
6
* found in the LICENSE file at https://angular.dev/license
7
7
*/
8
+ import { _IdGenerator } from '@angular/cdk/a11y' ;
8
9
import { Directionality } from '@angular/cdk/bidi' ;
9
10
import { BooleanInput , coerceBooleanProperty } from '@angular/cdk/coercion' ;
10
11
import { Platform } from '@angular/cdk/platform' ;
@@ -20,23 +21,23 @@ import {
20
21
ContentChildren ,
21
22
ElementRef ,
22
23
InjectionToken ,
23
- Injector ,
24
24
Input ,
25
25
NgZone ,
26
26
OnDestroy ,
27
27
QueryList ,
28
28
ViewChild ,
29
29
ViewEncapsulation ,
30
- afterRender ,
30
+ afterRenderEffect ,
31
31
computed ,
32
32
contentChild ,
33
33
inject ,
34
+ signal ,
35
+ viewChild ,
34
36
} from '@angular/core' ;
35
37
import { AbstractControlDirective , ValidatorFn } from '@angular/forms' ;
36
- import { _animationsDisabled , ThemePalette } from '../core' ;
37
- import { _IdGenerator } from '@angular/cdk/a11y' ;
38
38
import { 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' ;
40
41
import { MAT_ERROR , MatError } from './directives/error' ;
41
42
import {
42
43
FLOATING_LABEL_PARENT ,
@@ -189,7 +190,6 @@ export class MatFormField
189
190
private _platform = inject ( Platform ) ;
190
191
private _idGenerator = inject ( _IdGenerator ) ;
191
192
private _ngZone = inject ( NgZone ) ;
192
- private _injector = inject ( Injector ) ;
193
193
private _defaults = inject < MatFormFieldDefaultOptions > ( MAT_FORM_FIELD_DEFAULT_OPTIONS , {
194
194
optional : true ,
195
195
} ) ;
@@ -203,6 +203,21 @@ export class MatFormField
203
203
@ViewChild ( MatFormFieldNotchedOutline ) _notchedOutline : MatFormFieldNotchedOutline | undefined ;
204
204
@ViewChild ( MatFormFieldLineRipple ) _lineRipple : MatFormFieldLineRipple | undefined ;
205
205
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
+
206
221
@ContentChild ( _MatFormFieldControl ) _formFieldControl : MatFormFieldControl < any > ;
207
222
@ContentChildren ( MAT_PREFIX , { descendants : true } ) _prefixChildren : QueryList < MatPrefix > ;
208
223
@ContentChildren ( MAT_SUFFIX , { descendants : true } ) _suffixChildren : QueryList < MatSuffix > ;
@@ -250,10 +265,9 @@ export class MatFormField
250
265
/** The form field appearance style. */
251
266
@Input ( )
252
267
get appearance ( ) : MatFormFieldAppearance {
253
- return this . _appearance ;
268
+ return this . _appearanceSignal ( ) ;
254
269
}
255
270
set appearance ( value : MatFormFieldAppearance ) {
256
- const oldValue = this . _appearance ;
257
271
const newAppearance = value || this . _defaults ?. appearance || DEFAULT_APPEARANCE ;
258
272
if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
259
273
if ( newAppearance !== 'fill' && newAppearance !== 'outline' ) {
@@ -262,15 +276,9 @@ export class MatFormField
262
276
) ;
263
277
}
264
278
}
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 ) ;
272
280
}
273
- private _appearance : MatFormFieldAppearance = DEFAULT_APPEARANCE ;
281
+ private _appearanceSignal = signal ( DEFAULT_APPEARANCE ) ;
274
282
275
283
/**
276
284
* Whether the form field should reserve space for one line of hint/error text (default)
@@ -319,7 +327,6 @@ export class MatFormField
319
327
private _destroyed = new Subject < void > ( ) ;
320
328
private _isFocused : boolean | null = null ;
321
329
private _explicitFormFieldControl : MatFormFieldControl < any > ;
322
- private _needsOutlineLabelOffsetUpdate = false ;
323
330
private _previousControl : MatFormFieldControl < unknown > | null = null ;
324
331
private _previousControlValidatorFn : ValidatorFn | null = null ;
325
332
private _stateChanges : Subscription | undefined ;
@@ -341,6 +348,8 @@ export class MatFormField
341
348
this . color = defaults . color ;
342
349
}
343
350
}
351
+
352
+ this . _syncOutlineLabelOffset ( ) ;
344
353
}
345
354
346
355
ngAfterViewInit ( ) {
@@ -366,7 +375,6 @@ export class MatFormField
366
375
this . _assertFormFieldControl ( ) ;
367
376
this . _initializeSubscript ( ) ;
368
377
this . _initializePrefixAndSuffix ( ) ;
369
- this . _initializeOutlineLabelOffsetSubscriptions ( ) ;
370
378
}
371
379
372
380
ngAfterContentChecked ( ) {
@@ -399,6 +407,7 @@ export class MatFormField
399
407
}
400
408
401
409
ngOnDestroy ( ) {
410
+ this . _outlineLabelOffsetResizeObserver ?. disconnect ( ) ;
402
411
this . _stateChanges ?. unsubscribe ( ) ;
403
412
this . _valueChanges ?. unsubscribe ( ) ;
404
413
this . _describedByChanges ?. unsubscribe ( ) ;
@@ -546,34 +555,37 @@ export class MatFormField
546
555
) ;
547
556
}
548
557
558
+ private _outlineLabelOffsetResizeObserver : ResizeObserver | null = null ;
559
+
549
560
/**
550
561
* The floating label in the docked state needs to account for prefixes. The horizontal offset
551
562
* is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
552
563
* form field is added to the DOM. This method sets up all subscriptions which are needed to
553
564
* trigger the label offset update.
554
565
*/
555
- private _initializeOutlineLabelOffsetSubscriptions ( ) {
566
+ private _syncOutlineLabelOffset ( ) {
556
567
// 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
-
560
568
// TODO(mmalerba): Split this into separate `afterRender` calls using the `EarlyRead` and
561
569
// `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 ;
567
575
}
568
- } ,
569
- {
570
- injector : this . _injector ,
571
- } ,
572
- ) ;
573
576
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
+ } ) ;
577
589
}
578
590
579
591
/** Whether the floating label should always float or not. */
@@ -719,6 +731,7 @@ export class MatFormField
719
731
* incorporate the horizontal offset into their default text-field styles.
720
732
*/
721
733
private _updateOutlineLabelOffset ( ) {
734
+ const dir = this . _dir . valueSignal ( ) ;
722
735
if ( ! this . _hasOutline ( ) || ! this . _floatingLabel ) {
723
736
return ;
724
737
}
@@ -732,7 +745,6 @@ export class MatFormField
732
745
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
733
746
// the label offset update until the zone stabilizes.
734
747
if ( ! this . _isAttachedToDom ( ) ) {
735
- this . _needsOutlineLabelOffsetUpdate = true ;
736
748
return ;
737
749
}
738
750
const iconPrefixContainer = this . _iconPrefixContainer ?. nativeElement ;
@@ -745,7 +757,7 @@ export class MatFormField
745
757
const textSuffixContainerWidth = textSuffixContainer ?. getBoundingClientRect ( ) . width ?? 0 ;
746
758
// If the directionality is RTL, the x-axis transform needs to be inverted. This
747
759
// 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' ;
749
761
const prefixWidth = `${ iconPrefixContainerWidth + textPrefixContainerWidth } px` ;
750
762
const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)` ;
751
763
const labelHorizontalOffset = `calc(${ negate } * (${ prefixWidth } + ${ labelOffset } ))` ;
0 commit comments