diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index be4159cd5c84..7587ff7b0880 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -4015,6 +4015,84 @@ describe('MatSelect', () => { }); }); + + describe('value propagation when options are added/removed', () => { + let fixture: ComponentFixture; + let testComponent: BasicSelectWithoutFormsMultiple; + + beforeEach(fakeAsync(() => { + configureMatSelectTestingModule([BasicSelectWithoutFormsMultiple]); + fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple); + testComponent = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should propagate the changes when a selected option is removed', fakeAsync(() => { + testComponent.selectedFoods = ['steak-0', 'pizza-1']; + fixture.detectChanges(); + flush(); + + expect(testComponent.select.value).toEqual(['steak-0', 'pizza-1']); + testComponent.selectionChange.calls.reset(); + + testComponent.foods.shift(); + fixture.detectChanges(); + flush(); + + expect(testComponent.selectionChange).toHaveBeenCalledTimes(1); + expect(testComponent.select.value).toEqual(['pizza-1']); + expect(testComponent.selectedFoods).toEqual(['pizza-1']); + })); + + it('should propagate the changes when a selected option is added', fakeAsync(() => { + testComponent.selectedFoods = ['steak-0']; + fixture.detectChanges(); + flush(); + + expect(testComponent.select.value).toEqual(['steak-0']); + testComponent.selectionChange.calls.reset(); + + testComponent.foods.push({value: 'pasta-4', viewValue: 'Pasta'}); + testComponent.selectedFoods.push('pasta-4'); + fixture.detectChanges(); + flush(); + + expect(testComponent.selectionChange).toHaveBeenCalledTimes(1); + expect(testComponent.select.value).toEqual(['steak-0', 'pasta-4']); + expect(testComponent.selectedFoods).toEqual(['steak-0', 'pasta-4']); + })); + + it('should not propagate changes when a non-selected option is removed', fakeAsync(() => { + testComponent.selectedFoods = ['steak-0']; + fixture.detectChanges(); + flush(); + + testComponent.selectionChange.calls.reset(); + testComponent.foods.pop(); + fixture.detectChanges(); + flush(); + + expect(testComponent.selectionChange).not.toHaveBeenCalled(); + expect(testComponent.select.value).toEqual(['steak-0']); + expect(testComponent.selectedFoods).toEqual(['steak-0']); + })); + + it('should not propagate changes when a non-selected option is added', fakeAsync(() => { + testComponent.selectedFoods = ['steak-0']; + fixture.detectChanges(); + flush(); + + testComponent.selectionChange.calls.reset(); + testComponent.foods.push({value: 'pasta-4', viewValue: 'Pasta'}); + fixture.detectChanges(); + flush(); + + expect(testComponent.selectionChange).not.toHaveBeenCalled(); + expect(testComponent.select.value).toEqual(['steak-0']); + expect(testComponent.selectedFoods).toEqual(['steak-0']); + })); + + }); }); @@ -4601,7 +4679,8 @@ class BasicSelectWithoutFormsPreselected { @Component({ template: ` - + {{ food.viewValue }} @@ -4611,6 +4690,7 @@ class BasicSelectWithoutFormsPreselected { }) class BasicSelectWithoutFormsMultiple { selectedFoods: string[]; + selectionChange = jasmine.createSpy('selectionChange spy'); foods: any[] = [ { value: 'steak-0', viewValue: 'Steak' }, { value: 'pizza-1', viewValue: 'Pizza' }, diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index cd11b499f569..33964a193e97 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -519,9 +519,9 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, event.removed.forEach(option => option.deselect()); }); - this.options.changes.pipe(startWith(null), takeUntil(this._destroy)).subscribe(() => { + this.options.changes.pipe(startWith(null), takeUntil(this._destroy)).subscribe(options => { this._resetOptions(); - this._initializeSelection(); + options ? this._handleSelectionChange() : this._initializeSelection(); }); } @@ -781,6 +781,29 @@ export class MatSelect extends _MatSelectMixinBase implements AfterContentInit, }); } + /** + * Handles changes in the amount of options. Sets the `selected` status of any newly-added + * options and propagates the changes back up, if any of the selected options were removed + * or any selected options were added. + */ + private _handleSelectionChange(): void { + Promise.resolve().then(() => { + // Save the currently-selected options for reference. + const previousSelection = this._selectionModel.selected; + + // Update the selected options. + this._setSelectionByValue(this.ngControl ? this.ngControl.value : this._value); + + const currentSelection = this._selectionModel.selected; + + // Check if the selection has changed and propagate the changes to the model. + if (previousSelection.length !== currentSelection.length || + currentSelection.find(option => previousSelection.indexOf(option) === -1)) { + this._propagateChanges(); + } + }); + } + /** * Sets the selected option based on a value. If no option can be * found with the designated value, the select trigger is cleared.