Skip to content

Commit e921ebf

Browse files
committed
fix(material/select): disabled state out of sync when swapping form group with a disabled one
Fixes the disabled state of a `mat-select` falling out of sync with its form control if the control's group is swapped out with one that is disabled on init. Fixes #17860.
1 parent 2111bbf commit e921ebf

File tree

3 files changed

+112
-1
lines changed

3 files changed

+112
-1
lines changed

src/material-experimental/mdc-select/select.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
} from '@angular/core/testing';
4343
import {
4444
ControlValueAccessor,
45+
FormBuilder,
4546
FormControl,
4647
FormGroup,
4748
FormGroupDirective,
@@ -123,6 +124,7 @@ describe('MDC-based MatSelect', () => {
123124
SelectWithGroupsAndNgContainer,
124125
SelectWithFormFieldLabel,
125126
SelectWithChangeEvent,
127+
SelectInsideDynamicFormGroup,
126128
]);
127129
}),
128130
);
@@ -2193,6 +2195,23 @@ describe('MDC-based MatSelect', () => {
21932195
.withContext(`Expected select panelOpen property to become true.`)
21942196
.toBe(true);
21952197
}));
2198+
2199+
it(
2200+
'should keep the disabled state in sync if the form group is swapped and ' +
2201+
'disabled at the same time',
2202+
fakeAsync(() => {
2203+
const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup);
2204+
fixture.detectChanges();
2205+
const instance = fixture.componentInstance;
2206+
2207+
expect(instance.select.disabled).toBe(false);
2208+
2209+
instance.assignGroup(true);
2210+
fixture.detectChanges();
2211+
2212+
expect(instance.select.disabled).toBe(true);
2213+
}),
2214+
);
21962215
});
21972216

21982217
describe('keyboard scrolling', () => {
@@ -5036,3 +5055,29 @@ class SelectWithResetOptionAndFormControl {
50365055
`,
50375056
})
50385057
class SelectInNgContainer {}
5058+
5059+
@Component({
5060+
template: `
5061+
<form [formGroup]="form">
5062+
<mat-form-field>
5063+
<mat-select formControlName="control">
5064+
<mat-option value="1">One</mat-option>
5065+
</mat-select>
5066+
</mat-form-field>
5067+
</form>
5068+
`,
5069+
})
5070+
class SelectInsideDynamicFormGroup {
5071+
@ViewChild(MatSelect) select: MatSelect;
5072+
form: FormGroup;
5073+
5074+
constructor(private _formBuilder: FormBuilder) {
5075+
this.assignGroup(false);
5076+
}
5077+
5078+
assignGroup(isDisabled: boolean) {
5079+
this.form = this._formBuilder.group({
5080+
control: {value: '', disabled: isDisabled},
5081+
});
5082+
}
5083+
}

src/material/select/select.spec.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import {
4949
NG_VALUE_ACCESSOR,
5050
ReactiveFormsModule,
5151
Validators,
52+
FormBuilder,
5253
} from '@angular/forms';
5354
import {ErrorStateMatcher, MatOption, MatOptionSelectionChange} from '@angular/material/core';
5455
import {
@@ -122,6 +123,7 @@ describe('MatSelect', () => {
122123
SelectWithGroupsAndNgContainer,
123124
SelectWithFormFieldLabel,
124125
SelectWithChangeEvent,
126+
SelectInsideDynamicFormGroup,
125127
]);
126128
}),
127129
);
@@ -2208,6 +2210,23 @@ describe('MatSelect', () => {
22082210
.withContext(`Expected select panelOpen property to become true.`)
22092211
.toBe(true);
22102212
}));
2213+
2214+
it(
2215+
'should keep the disabled state in sync if the form group is swapped and ' +
2216+
'disabled at the same time',
2217+
fakeAsync(() => {
2218+
const fixture = TestBed.createComponent(SelectInsideDynamicFormGroup);
2219+
fixture.detectChanges();
2220+
const instance = fixture.componentInstance;
2221+
2222+
expect(instance.select.disabled).toBe(false);
2223+
2224+
instance.assignGroup(true);
2225+
fixture.detectChanges();
2226+
2227+
expect(instance.select.disabled).toBe(true);
2228+
}),
2229+
);
22112230
});
22122231

22132232
describe('animations', () => {
@@ -6017,3 +6036,29 @@ class SelectWithResetOptionAndFormControl {
60176036
`,
60186037
})
60196038
class SelectInNgContainer {}
6039+
6040+
@Component({
6041+
template: `
6042+
<form [formGroup]="form">
6043+
<mat-form-field>
6044+
<mat-select formControlName="control">
6045+
<mat-option value="1">One</mat-option>
6046+
</mat-select>
6047+
</mat-form-field>
6048+
</form>
6049+
`,
6050+
})
6051+
class SelectInsideDynamicFormGroup {
6052+
@ViewChild(MatSelect) select: MatSelect;
6053+
form: FormGroup;
6054+
6055+
constructor(private _formBuilder: FormBuilder) {
6056+
this.assignGroup(false);
6057+
}
6058+
6059+
assignGroup(isDisabled: boolean) {
6060+
this.form = this._formBuilder.group({
6061+
control: {value: '', disabled: isDisabled},
6062+
});
6063+
}
6064+
}

src/material/select/select.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
ViewEncapsulation,
6161
} from '@angular/core';
6262
import {
63+
AbstractControl,
6364
ControlValueAccessor,
6465
FormGroupDirective,
6566
NgControl,
@@ -293,6 +294,12 @@ export abstract class _MatSelectBase<C>
293294
/** Current `ariar-labelledby` value for the select trigger. */
294295
private _triggerAriaLabelledBy: string | null = null;
295296

297+
/**
298+
* Keeps track of the previous form control assigned to the select.
299+
* Used to detect if it has changed.
300+
*/
301+
private _previousControl: AbstractControl | null | undefined;
302+
296303
/** Emits whenever the component is destroyed. */
297304
protected readonly _destroy = new Subject<void>();
298305

@@ -571,6 +578,7 @@ export abstract class _MatSelectBase<C>
571578

572579
ngDoCheck() {
573580
const newAriaLabelledby = this._getTriggerAriaLabelledby();
581+
const ngControl = this.ngControl;
574582

575583
// We have to manage setting the `aria-labelledby` ourselves, because part of its value
576584
// is computed as a result of a content query which can cause this binding to trigger a
@@ -585,7 +593,20 @@ export abstract class _MatSelectBase<C>
585593
}
586594
}
587595

588-
if (this.ngControl) {
596+
if (ngControl) {
597+
// The disabled state might go out of sync if the form group is swapped out. See #17860.
598+
if (this._previousControl !== ngControl.control) {
599+
if (
600+
this._previousControl !== undefined &&
601+
ngControl.disabled !== null &&
602+
ngControl.disabled !== this.disabled
603+
) {
604+
this.disabled = ngControl.disabled;
605+
}
606+
607+
this._previousControl = ngControl.control;
608+
}
609+
589610
this.updateErrorState();
590611
}
591612
}

0 commit comments

Comments
 (0)