Skip to content

fix(material/form-field): trigger CD when form gets reassigned #30395

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion src/material/form-field/form-field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -322,6 +322,7 @@ export class MatFormField
private _explicitFormFieldControl: MatFormFieldControl<any>;
private _needsOutlineLabelOffsetUpdate = false;
private _previousControl: MatFormFieldControl<unknown> | null = null;
private _previousControlValidatorFn: ValidatorFn | null = null;
private _stateChanges: Subscription | undefined;
private _valueChanges: Subscription | undefined;
private _describedByChanges: Subscription | undefined;
Expand Down Expand Up @@ -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() {
Expand Down
68 changes: 68 additions & 0 deletions src/material/input/input.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,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);

Expand Down Expand Up @@ -2324,3 +2366,29 @@ class MatInputSimple {}
standalone: false,
})
class InputWithNgContainerPrefixAndSuffix {}

@Component({
template: `
<mat-form-field>
<mat-label>Hello</mat-label>
<input matInput [formControl]="formControl">
</mat-form-field>`,
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();
}
}
Loading