Skip to content

Commit f61a213

Browse files
committed
fix(material/select): changed after checked error if option label changes (#23315)
Fixes a "changed after checked" error that is thrown if the label of a selected option changes dynamically. This is alternate approach to #14797 which was tricky to land, because it introduced an extra timeout. Fixes #14793. (cherry picked from commit 52a36b3)
1 parent 52e8f44 commit f61a213

File tree

4 files changed

+38
-4
lines changed

4 files changed

+38
-4
lines changed

src/material/core/option/option.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,11 @@ export class _MatOptionBase<T = any> implements FocusableOption, AfterViewChecke
225225
const viewValue = this.viewValue;
226226

227227
if (viewValue !== this._mostRecentViewValue) {
228+
if (this._mostRecentViewValue) {
229+
this._stateChanges.next();
230+
}
231+
228232
this._mostRecentViewValue = viewValue;
229-
this._stateChanges.next();
230233
}
231234
}
232235
}

src/material/legacy-select/select.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1831,6 +1831,19 @@ describe('MatSelect', () => {
18311831
expect(trigger.textContent!.trim()).toBe('Calzone');
18321832
}));
18331833

1834+
it('should update the trigger value if the text as a result of an expression change', fakeAsync(() => {
1835+
fixture.componentInstance.control.setValue('pizza-1');
1836+
fixture.detectChanges();
1837+
1838+
expect(trigger.textContent!.trim()).toBe('Pizza');
1839+
1840+
fixture.componentInstance.capitalize = true;
1841+
fixture.detectChanges();
1842+
fixture.checkNoChanges();
1843+
1844+
expect(trigger.textContent!.trim()).toBe('PIZZA');
1845+
}));
1846+
18341847
it('should not select disabled options', fakeAsync(() => {
18351848
trigger.click();
18361849
fixture.detectChanges();
@@ -5202,7 +5215,7 @@ describe('MatSelect', () => {
52025215
[panelClass]="panelClass" [disableRipple]="disableRipple"
52035216
[typeaheadDebounceInterval]="typeaheadDebounceInterval">
52045217
<mat-option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
5205-
{{ food.viewValue }}
5218+
{{ capitalize ? food.viewValue.toUpperCase() : food.viewValue }}
52065219
</mat-option>
52075220
</mat-select>
52085221
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
@@ -5233,6 +5246,7 @@ class BasicSelect {
52335246
panelClass = ['custom-one', 'custom-two'];
52345247
disableRipple: boolean;
52355248
typeaheadDebounceInterval: number;
5249+
capitalize = false;
52365250

52375251
@ViewChild(MatLegacySelect, {static: true}) select: MatLegacySelect;
52385252
@ViewChildren(MatLegacyOption) options: QueryList<MatLegacyOption>;

src/material/select/select.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1879,6 +1879,19 @@ describe('MDC-based MatSelect', () => {
18791879
expect(trigger.textContent!.trim()).toBe('Calzone');
18801880
}));
18811881

1882+
it('should update the trigger value if the text as a result of an expression change', fakeAsync(() => {
1883+
fixture.componentInstance.control.setValue('pizza-1');
1884+
fixture.detectChanges();
1885+
1886+
expect(trigger.textContent!.trim()).toBe('Pizza');
1887+
1888+
fixture.componentInstance.capitalize = true;
1889+
fixture.detectChanges();
1890+
fixture.checkNoChanges();
1891+
1892+
expect(trigger.textContent!.trim()).toBe('PIZZA');
1893+
}));
1894+
18821895
it('should not select disabled options', fakeAsync(() => {
18831896
trigger.click();
18841897
fixture.detectChanges();
@@ -4410,7 +4423,7 @@ describe('MDC-based MatSelect', () => {
44104423
[panelClass]="panelClass" [disableRipple]="disableRipple"
44114424
[typeaheadDebounceInterval]="typeaheadDebounceInterval">
44124425
<mat-option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
4413-
{{ food.viewValue }}
4426+
{{ capitalize ? food.viewValue.toUpperCase() : food.viewValue }}
44144427
</mat-option>
44154428
</mat-select>
44164429
<mat-hint *ngIf="hint">{{ hint }}</mat-hint>
@@ -4442,6 +4455,7 @@ class BasicSelect {
44424455
panelClass = ['custom-one', 'custom-two'];
44434456
disableRipple: boolean;
44444457
typeaheadDebounceInterval: number;
4458+
capitalize = false;
44454459

44464460
@ViewChild(MatSelect, {static: true}) select: MatSelect;
44474461
@ViewChildren(MatOption) options: QueryList<MatOption>;

src/material/select/select.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -961,7 +961,10 @@ export abstract class _MatSelectBase<C>
961961
merge(...this.options.map(option => option._stateChanges))
962962
.pipe(takeUntil(changedOrDestroyed))
963963
.subscribe(() => {
964-
this._changeDetectorRef.markForCheck();
964+
// `_stateChanges` can fire as a result of a change in the label's DOM value which may
965+
// be the result of an expression changing. We have to use `detectChanges` in order
966+
// to avoid "changed after checked" errors (see #14793).
967+
this._changeDetectorRef.detectChanges();
965968
this.stateChanges.next();
966969
});
967970
}

0 commit comments

Comments
 (0)