From e70145b0fb8609eddbb3ed6b8cba15b7271b0b69 Mon Sep 17 00:00:00 2001 From: Naji <54370141+naaajii@users.noreply.github.com> Date: Thu, 20 Feb 2025 12:09:58 +0500 Subject: [PATCH] fix(material/form-field): trigger CD when form (#30395) gets reassigned fixes the issue we were not marking component for changes when form is reassigned making it not update UI for required asterisk fixes #29066 (cherry picked from commit cdb15925494fa83bf1e4033b49e42d6649e43deb) --- src/material/form-field/form-field.ts | 23 ++++++++- src/material/input/input.spec.ts | 68 +++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/material/form-field/form-field.ts b/src/material/form-field/form-field.ts index 0ea473c78d41..0aa19d044ccc 100644 --- a/src/material/form-field/form-field.ts +++ b/src/material/form-field/form-field.ts @@ -33,7 +33,7 @@ import { contentChild, inject, } from '@angular/core'; -import {AbstractControlDirective} from '@angular/forms'; +import {AbstractControlDirective, ValidatorFn} from '@angular/forms'; import {ThemePalette} from '@angular/material/core'; import {_IdGenerator} from '@angular/cdk/a11y'; import {Subject, Subscription, merge} from 'rxjs'; @@ -322,6 +322,7 @@ export class MatFormField private _explicitFormFieldControl: MatFormFieldControl; private _needsOutlineLabelOffsetUpdate = false; private _previousControl: MatFormFieldControl | null = null; + private _previousControlValidatorFn: ValidatorFn | null = null; private _stateChanges: Subscription | undefined; private _valueChanges: Subscription | undefined; private _describedByChanges: Subscription | undefined; @@ -374,10 +375,30 @@ export class MatFormField ngAfterContentChecked() { this._assertFormFieldControl(); + // if form field was being used with an input in first place and then replaced by other + // component such as select. if (this._control !== this._previousControl) { this._initializeControl(this._previousControl); + + // keep a reference for last validator we had. + if (this._control.ngControl && this._control.ngControl.control) { + this._previousControlValidatorFn = this._control.ngControl.control.validator; + } + this._previousControl = this._control; } + + // make sure the the control has been initialized. + if (this._control.ngControl && this._control.ngControl.control) { + // get the validators for current control. + const validatorFn = this._control.ngControl.control.validator; + + // if our current validatorFn isn't equal to it might be we are CD behind, marking the + // component will allow us to catch up. + if (validatorFn !== this._previousControlValidatorFn) { + this._changeDetectorRef.markForCheck(); + } + } } ngOnDestroy() { diff --git a/src/material/input/input.spec.ts b/src/material/input/input.spec.ts index 8ecedf942c60..20eeff03e5ee 100644 --- a/src/material/input/input.spec.ts +++ b/src/material/input/input.spec.ts @@ -348,6 +348,48 @@ describe('MatMdcInput without forms', () => { expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy(); })); + it('should show the required star when FormControl is reassigned', fakeAsync(() => { + const fixture = createComponent(MatInputWithRequiredAssignableFormControl); + fixture.detectChanges(); + + // should have star by default + let label = fixture.debugElement.query(By.css('label'))!; + expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy(); + + fixture.componentInstance.reassignFormControl(); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // should be removed as form was reassigned with no required validators + label = fixture.debugElement.query(By.css('label'))!; + expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeFalsy(); + })); + + it('should show the required star when required validator is toggled', fakeAsync(() => { + const fixture = createComponent(MatInputWithRequiredAssignableFormControl); + fixture.detectChanges(); + + // should have star by default + let label = fixture.debugElement.query(By.css('label'))!; + expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy(); + + fixture.componentInstance.removeRequiredValidtor(); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // should be removed as control validator was removed + label = fixture.debugElement.query(By.css('label'))!; + expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeFalsy(); + + fixture.componentInstance.addRequiredValidator(); + fixture.changeDetectorRef.markForCheck(); + fixture.detectChanges(); + + // should contain star as control validator was readded + label = fixture.debugElement.query(By.css('label'))!; + expect(label.nativeElement.querySelector('.mat-mdc-form-field-required-marker')).toBeTruthy(); + })); + it('should not hide the required star if input is disabled', () => { const fixture = createComponent(MatInputLabelRequiredTestComponent); @@ -2321,3 +2363,29 @@ class MatInputSimple {} standalone: false, }) class InputWithNgContainerPrefixAndSuffix {} + +@Component({ + template: ` + + Hello + + `, + standalone: false, +}) +class MatInputWithRequiredAssignableFormControl { + formControl = new FormControl('', [Validators.required]); + + reassignFormControl() { + this.formControl = new FormControl(); + } + + addRequiredValidator() { + this.formControl.setValidators([Validators.required]); + this.formControl.updateValueAndValidity(); + } + + removeRequiredValidtor() { + this.formControl.setValidators([]); + this.formControl.updateValueAndValidity(); + } +}