Skip to content

Commit cf21674

Browse files
committed
refactor(material/form-field): use afterNextRender to schedule outline updates
1 parent 63a764d commit cf21674

File tree

1 file changed

+46
-57
lines changed

1 file changed

+46
-57
lines changed

src/material/form-field/form-field.ts

Lines changed: 46 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
ViewChild,
2828
ViewEncapsulation,
2929
ANIMATION_MODULE_TYPE,
30+
afterNextRender,
3031
} from '@angular/core';
3132
import {AbstractControlDirective} from '@angular/forms';
3233
import {ThemePalette} from '@angular/material/core';
@@ -245,7 +246,7 @@ export class MatFormField
245246
// If the appearance has been switched to `outline`, the label offset needs to be updated.
246247
// The update can happen once the view has been re-checked, but not immediately because
247248
// the view has not been updated and the notched-outline floating label is not present.
248-
this._needsOutlineLabelOffsetUpdateOnStable = true;
249+
this._updateOutlineLabelOffsetAfterNextRender();
249250
}
250251
}
251252
private _appearance: MatFormFieldAppearance = DEFAULT_APPEARANCE;
@@ -300,11 +301,14 @@ export class MatFormField
300301
private _destroyed = new Subject<void>();
301302
private _isFocused: boolean | null = null;
302303
private _explicitFormFieldControl: MatFormFieldControl<any>;
303-
private _needsOutlineLabelOffsetUpdateOnStable = false;
304304

305305
constructor(
306306
public _elementRef: ElementRef,
307307
private _changeDetectorRef: ChangeDetectorRef,
308+
/**
309+
* @deprecated not needed, to be removed.
310+
* @breaking-change 19.0.0 remove this param
311+
*/
308312
private _ngZone: NgZone,
309313
private _dir: Directionality,
310314
private _platform: Platform,
@@ -485,30 +489,13 @@ export class MatFormField
485489
* The floating label in the docked state needs to account for prefixes. The horizontal offset
486490
* is calculated whenever the appearance changes to `outline`, the prefixes change, or when the
487491
* form field is added to the DOM. This method sets up all subscriptions which are needed to
488-
* trigger the label offset update. In general, we want to avoid performing measurements often,
489-
* so we rely on the `NgZone` as indicator when the offset should be recalculated, instead of
490-
* checking every change detection cycle.
492+
* trigger the label offset update.
491493
*/
492494
private _initializeOutlineLabelOffsetSubscriptions() {
493-
// Whenever the prefix changes, schedule an update of the label offset.
494-
this._prefixChildren.changes.subscribe(
495-
() => (this._needsOutlineLabelOffsetUpdateOnStable = true),
496-
);
497-
498-
// Note that we have to run outside of the `NgZone` explicitly, in order to avoid
499-
// throwing users into an infinite loop if `zone-patch-rxjs` is included.
500-
this._ngZone.runOutsideAngular(() => {
501-
this._ngZone.onStable.pipe(takeUntil(this._destroyed)).subscribe(() => {
502-
if (this._needsOutlineLabelOffsetUpdateOnStable) {
503-
this._needsOutlineLabelOffsetUpdateOnStable = false;
504-
this._updateOutlineLabelOffset();
505-
}
506-
});
507-
});
508-
495+
this._prefixChildren.changes.subscribe(() => this._updateOutlineLabelOffsetAfterNextRender());
509496
this._dir.change
510497
.pipe(takeUntil(this._destroyed))
511-
.subscribe(() => (this._needsOutlineLabelOffsetUpdateOnStable = true));
498+
.subscribe(() => this._updateOutlineLabelOffsetAfterNextRender());
512499
}
513500

514501
/** Whether the floating label should always float or not. */
@@ -652,41 +639,43 @@ export class MatFormField
652639
* not need to do this because they use a fixed width for prefixes. Hence, they can simply
653640
* incorporate the horizontal offset into their default text-field styles.
654641
*/
655-
private _updateOutlineLabelOffset() {
656-
if (!this._platform.isBrowser || !this._hasOutline() || !this._floatingLabel) {
657-
return;
658-
}
659-
const floatingLabel = this._floatingLabel.element;
660-
// If no prefix is displayed, reset the outline label offset from potential
661-
// previous label offset updates.
662-
if (!(this._iconPrefixContainer || this._textPrefixContainer)) {
663-
floatingLabel.style.transform = '';
664-
return;
665-
}
666-
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
667-
// the label offset update until the zone stabilizes.
668-
if (!this._isAttachedToDom()) {
669-
this._needsOutlineLabelOffsetUpdateOnStable = true;
670-
return;
671-
}
672-
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
673-
const textPrefixContainer = this._textPrefixContainer?.nativeElement;
674-
const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0;
675-
const textPrefixContainerWidth = textPrefixContainer?.getBoundingClientRect().width ?? 0;
676-
// If the directionality is RTL, the x-axis transform needs to be inverted. This
677-
// is because `transformX` does not change based on the page directionality.
678-
const negate = this._dir.value === 'rtl' ? '-1' : '1';
679-
const prefixWidth = `${iconPrefixContainerWidth + textPrefixContainerWidth}px`;
680-
const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)`;
681-
const labelHorizontalOffset = `calc(${negate} * (${prefixWidth} + ${labelOffset}))`;
682-
683-
// Update the translateX of the floating label to account for the prefix container,
684-
// but allow the CSS to override this setting via a CSS variable when the label is
685-
// floating.
686-
floatingLabel.style.transform = `var(
687-
--mat-mdc-form-field-label-transform,
688-
${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset})
689-
)`;
642+
private _updateOutlineLabelOffsetAfterNextRender() {
643+
afterNextRender(() => {
644+
if (!this._platform.isBrowser || !this._hasOutline() || !this._floatingLabel) {
645+
return;
646+
}
647+
const floatingLabel = this._floatingLabel.element;
648+
// If no prefix is displayed, reset the outline label offset from potential
649+
// previous label offset updates.
650+
if (!(this._iconPrefixContainer || this._textPrefixContainer)) {
651+
floatingLabel.style.transform = '';
652+
return;
653+
}
654+
// If the form field is not attached to the DOM yet (e.g. in a tab), we defer
655+
// the label offset update until after the next render.
656+
if (!this._isAttachedToDom()) {
657+
this._updateOutlineLabelOffsetAfterNextRender();
658+
return;
659+
}
660+
const iconPrefixContainer = this._iconPrefixContainer?.nativeElement;
661+
const textPrefixContainer = this._textPrefixContainer?.nativeElement;
662+
const iconPrefixContainerWidth = iconPrefixContainer?.getBoundingClientRect().width ?? 0;
663+
const textPrefixContainerWidth = textPrefixContainer?.getBoundingClientRect().width ?? 0;
664+
// If the directionality is RTL, the x-axis transform needs to be inverted. This
665+
// is because `transformX` does not change based on the page directionality.
666+
const negate = this._dir.value === 'rtl' ? '-1' : '1';
667+
const prefixWidth = `${iconPrefixContainerWidth + textPrefixContainerWidth}px`;
668+
const labelOffset = `var(--mat-mdc-form-field-label-offset-x, 0px)`;
669+
const labelHorizontalOffset = `calc(${negate} * (${prefixWidth} + ${labelOffset}))`;
670+
671+
// Update the translateX of the floating label to account for the prefix container,
672+
// but allow the CSS to override this setting via a CSS variable when the label is
673+
// floating.
674+
floatingLabel.style.transform = `var(
675+
--mat-mdc-form-field-label-transform,
676+
${FLOATING_LABEL_DEFAULT_DOCKED_TRANSFORM} translateX(${labelHorizontalOffset})
677+
)`;
678+
});
690679
}
691680

692681
/** Checks whether the form field is attached to the DOM. */

0 commit comments

Comments
 (0)