Skip to content

Commit 77b11f4

Browse files
authored
fix(select): emitting change event twice for reset values (#13598)
Fixes `mat-select` emitting its change event twice when a reset value is selected, as well as when it's selected twice in a row. This PR covers #10859 which would've introduced another issue. Fixes #10675. Fixes #13579.
1 parent a388cc3 commit 77b11f4

File tree

2 files changed

+124
-5
lines changed

2 files changed

+124
-5
lines changed

src/material/select/select.spec.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2910,6 +2910,44 @@ describe('MatSelect', () => {
29102910
}));
29112911
});
29122912

2913+
describe('with reset option and a form control', () => {
2914+
let fixture: ComponentFixture<SelectWithResetOptionAndFormControl>;
2915+
let options: HTMLElement[];
2916+
2917+
beforeEach(fakeAsync(() => {
2918+
configureMatSelectTestingModule([SelectWithResetOptionAndFormControl]);
2919+
fixture = TestBed.createComponent(SelectWithResetOptionAndFormControl);
2920+
fixture.detectChanges();
2921+
fixture.debugElement.query(By.css('.mat-select-trigger'))!.nativeElement.click();
2922+
fixture.detectChanges();
2923+
options = Array.from(overlayContainerElement.querySelectorAll('mat-option'));
2924+
}));
2925+
2926+
it('should set the select value', fakeAsync(() => {
2927+
fixture.componentInstance.control.setValue('a');
2928+
fixture.detectChanges();
2929+
expect(fixture.componentInstance.select.value).toBe('a');
2930+
}));
2931+
2932+
it('should reset the control value', fakeAsync(() => {
2933+
fixture.componentInstance.control.setValue('a');
2934+
fixture.detectChanges();
2935+
2936+
options[0].click();
2937+
fixture.detectChanges();
2938+
flush();
2939+
expect(fixture.componentInstance.control.value).toBe(undefined);
2940+
}));
2941+
2942+
it('should reflect the value in the form control', fakeAsync(() => {
2943+
options[1].click();
2944+
fixture.detectChanges();
2945+
flush();
2946+
expect(fixture.componentInstance.select.value).toBe('a');
2947+
expect(fixture.componentInstance.control.value).toBe('a');
2948+
}));
2949+
});
2950+
29132951
describe('without Angular forms', () => {
29142952
beforeEach(async(() => configureMatSelectTestingModule([
29152953
BasicSelectWithoutForms,
@@ -3187,6 +3225,63 @@ describe('MatSelect', () => {
31873225
.toBeFalsy('Expected no value after tabbing away.');
31883226
}));
31893227

3228+
it('should emit once when a reset value is selected', fakeAsync(() => {
3229+
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
3230+
const instance = fixture.componentInstance;
3231+
const spy = jasmine.createSpy('change spy');
3232+
3233+
instance.selectedFood = 'sandwich-2';
3234+
instance.foods[0].value = null;
3235+
fixture.detectChanges();
3236+
3237+
const subscription = instance.select.selectionChange.subscribe(spy);
3238+
3239+
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
3240+
fixture.detectChanges();
3241+
flush();
3242+
3243+
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
3244+
fixture.detectChanges();
3245+
flush();
3246+
3247+
expect(spy).toHaveBeenCalledTimes(1);
3248+
3249+
subscription.unsubscribe();
3250+
}));
3251+
3252+
it('should not emit the change event multiple times when a reset option is ' +
3253+
'selected twice in a row', fakeAsync(() => {
3254+
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
3255+
const instance = fixture.componentInstance;
3256+
const spy = jasmine.createSpy('change spy');
3257+
3258+
instance.foods[0].value = null;
3259+
fixture.detectChanges();
3260+
3261+
const subscription = instance.select.selectionChange.subscribe(spy);
3262+
3263+
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
3264+
fixture.detectChanges();
3265+
flush();
3266+
3267+
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
3268+
fixture.detectChanges();
3269+
flush();
3270+
3271+
expect(spy).not.toHaveBeenCalled();
3272+
3273+
fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement.click();
3274+
fixture.detectChanges();
3275+
flush();
3276+
3277+
(overlayContainerElement.querySelector('mat-option') as HTMLElement).click();
3278+
fixture.detectChanges();
3279+
flush();
3280+
3281+
expect(spy).not.toHaveBeenCalled();
3282+
3283+
subscription.unsubscribe();
3284+
}));
31903285

31913286
});
31923287

@@ -5315,3 +5410,23 @@ class MultiSelectWithLotsOfOptions {
53155410
this.value = [];
53165411
}
53175412
}
5413+
5414+
5415+
@Component({
5416+
selector: 'basic-select-with-reset',
5417+
template: `
5418+
<mat-form-field>
5419+
<mat-select [formControl]="control">
5420+
<mat-option>Reset</mat-option>
5421+
<mat-option value="a">A</mat-option>
5422+
<mat-option value="b">B</mat-option>
5423+
<mat-option value="c">C</mat-option>
5424+
</mat-select>
5425+
</mat-form-field>
5426+
`
5427+
})
5428+
class SelectWithResetOptionAndFormControl {
5429+
@ViewChild(MatSelect) select: MatSelect;
5430+
@ViewChildren(MatOption) options: QueryList<MatOption>;
5431+
control = new FormControl();
5432+
}

src/material/select/select.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,10 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
442442
get value(): any { return this._value; }
443443
set value(newValue: any) {
444444
if (newValue !== this._value) {
445-
this.writeValue(newValue);
445+
if (this.options) {
446+
this._setSelectionByValue(newValue);
447+
}
448+
446449
this._value = newValue;
447450
}
448451
}
@@ -676,9 +679,7 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
676679
* @param value New value to be written to the model.
677680
*/
678681
writeValue(value: any): void {
679-
if (this.options) {
680-
this._setSelectionByValue(value);
681-
}
682+
this.value = value;
682683
}
683684

684685
/**
@@ -1003,7 +1004,10 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit,
10031004
if (option.value == null && !this._multiple) {
10041005
option.deselect();
10051006
this._selectionModel.clear();
1006-
this._propagateChanges(option.value);
1007+
1008+
if (this.value != null) {
1009+
this._propagateChanges(option.value);
1010+
}
10071011
} else {
10081012
if (wasSelected !== option.selected) {
10091013
option.selected ? this._selectionModel.select(option) :

0 commit comments

Comments
 (0)