diff --git a/src/material-experimental/mdc-select/select.spec.ts b/src/material-experimental/mdc-select/select.spec.ts index 06ec2d09ee67..1f89790daea9 100644 --- a/src/material-experimental/mdc-select/select.spec.ts +++ b/src/material-experimental/mdc-select/select.spec.ts @@ -42,6 +42,7 @@ import { } from '@angular/core/testing'; import { ControlValueAccessor, + FormBuilder, FormControl, FormGroup, FormGroupDirective, @@ -123,6 +124,7 @@ describe('MDC-based MatSelect', () => { SelectWithGroupsAndNgContainer, SelectWithFormFieldLabel, SelectWithChangeEvent, + SelectInsideDynamicFormGroup, ]); }), ); @@ -2193,6 +2195,23 @@ describe('MDC-based MatSelect', () => { .withContext(`Expected select panelOpen property to become true.`) .toBe(true); })); + + it( + 'should keep the disabled state in sync if the form group is swapped and ' + + 'disabled at the same time', + fakeAsync(() => { + const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup); + fixture.detectChanges(); + const instance = fixture.componentInstance; + + expect(instance.select.disabled).toBe(false); + + instance.assignGroup(true); + fixture.detectChanges(); + + expect(instance.select.disabled).toBe(true); + }), + ); }); describe('keyboard scrolling', () => { @@ -5036,3 +5055,29 @@ class SelectWithResetOptionAndFormControl { `, }) class SelectInNgContainer {} + +@Component({ + template: ` +
+ + + One + + +
+ `, +}) +class SelectInsideDynamicFormGroup { + @ViewChild(MatSelect) select: MatSelect; + form: FormGroup; + + constructor(private _formBuilder: FormBuilder) { + this.assignGroup(false); + } + + assignGroup(isDisabled: boolean) { + this.form = this._formBuilder.group({ + control: {value: '', disabled: isDisabled}, + }); + } +} diff --git a/src/material/select/select.spec.ts b/src/material/select/select.spec.ts index 4ba42d18b7d0..a0cd6a819e10 100644 --- a/src/material/select/select.spec.ts +++ b/src/material/select/select.spec.ts @@ -49,6 +49,7 @@ import { NG_VALUE_ACCESSOR, ReactiveFormsModule, Validators, + FormBuilder, } from '@angular/forms'; import {ErrorStateMatcher, MatOption, MatOptionSelectionChange} from '@angular/material/core'; import { @@ -122,6 +123,7 @@ describe('MatSelect', () => { SelectWithGroupsAndNgContainer, SelectWithFormFieldLabel, SelectWithChangeEvent, + SelectInsideDynamicFormGroup, ]); }), ); @@ -2208,6 +2210,23 @@ describe('MatSelect', () => { .withContext(`Expected select panelOpen property to become true.`) .toBe(true); })); + + it( + 'should keep the disabled state in sync if the form group is swapped and ' + + 'disabled at the same time', + fakeAsync(() => { + const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup); + fixture.detectChanges(); + const instance = fixture.componentInstance; + + expect(instance.select.disabled).toBe(false); + + instance.assignGroup(true); + fixture.detectChanges(); + + expect(instance.select.disabled).toBe(true); + }), + ); }); describe('animations', () => { @@ -6017,3 +6036,29 @@ class SelectWithResetOptionAndFormControl { `, }) class SelectInNgContainer {} + +@Component({ + template: ` +
+ + + One + + +
+ `, +}) +class SelectInsideDynamicFormGroup { + @ViewChild(MatSelect) select: MatSelect; + form: FormGroup; + + constructor(private _formBuilder: FormBuilder) { + this.assignGroup(false); + } + + assignGroup(isDisabled: boolean) { + this.form = this._formBuilder.group({ + control: {value: '', disabled: isDisabled}, + }); + } +} diff --git a/src/material/select/select.ts b/src/material/select/select.ts index ecd5e82b6ab8..b6444f4b7430 100644 --- a/src/material/select/select.ts +++ b/src/material/select/select.ts @@ -60,6 +60,7 @@ import { ViewEncapsulation, } from '@angular/core'; import { + AbstractControl, ControlValueAccessor, FormGroupDirective, NgControl, @@ -281,6 +282,12 @@ export abstract class _MatSelectBase /** Current `ariar-labelledby` value for the select trigger. */ private _triggerAriaLabelledBy: string | null = null; + /** + * Keeps track of the previous form control assigned to the select. + * Used to detect if it has changed. + */ + private _previousControl: AbstractControl | null | undefined; + /** Emits whenever the component is destroyed. */ protected readonly _destroy = new Subject(); @@ -559,6 +566,7 @@ export abstract class _MatSelectBase ngDoCheck() { const newAriaLabelledby = this._getTriggerAriaLabelledby(); + const ngControl = this.ngControl; // We have to manage setting the `aria-labelledby` ourselves, because part of its value // is computed as a result of a content query which can cause this binding to trigger a @@ -573,7 +581,20 @@ export abstract class _MatSelectBase } } - if (this.ngControl) { + if (ngControl) { + // The disabled state might go out of sync if the form group is swapped out. See #17860. + if (this._previousControl !== ngControl.control) { + if ( + this._previousControl !== undefined && + ngControl.disabled !== null && + ngControl.disabled !== this.disabled + ) { + this.disabled = ngControl.disabled; + } + + this._previousControl = ngControl.control; + } + this.updateErrorState(); } }