fix(forms): ensure select[multiple] retains selections

If you bound an array to select[multiple] via ngModel and subsequently
changed the options to select from, the UI would drop any selections
made since by the user. This was due to
SelectMultipleControlValueAccessor not keeping a reference to the new
model arrays it generated when users interacted with the select control.
Update code to keep the reference.

Closes #12527
Closes #12654
This commit is contained in:
gary-b 2016-11-01 22:43:13 +00:00 committed by Victor Berchet
parent 2bf1bbc071
commit 821b8f09d6
2 changed files with 36 additions and 30 deletions

View File

@ -99,6 +99,7 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
} }
} }
} }
this.value = selected;
fn(selected); fn(selected);
}; };
} }

View File

@ -765,12 +765,23 @@ export function main() {
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}]; comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
}); });
const setSelectedCities = (selectedCities: any): void => { const detectChangesAndTick = (): void => {
comp.selectedCities = selectedCities;
fixture.detectChanges(); fixture.detectChanges();
tick(); tick();
}; };
const setSelectedCities = (selectedCities: any): void => {
comp.selectedCities = selectedCities;
detectChangesAndTick();
};
const selectOptionViaUI = (valueString: string): void => {
const select = fixture.debugElement.query(By.css('select'));
select.nativeElement.value = valueString;
dispatchEvent(select.nativeElement, 'change');
detectChangesAndTick();
};
const assertOptionElementSelectedState = (selectedStates: boolean[]): void => { const assertOptionElementSelectedState = (selectedStates: boolean[]): void => {
const options = fixture.debugElement.queryAll(By.css('option')); const options = fixture.debugElement.queryAll(By.css('option'));
if (options.length !== selectedStates.length) { if (options.length !== selectedStates.length) {
@ -781,36 +792,30 @@ export function main() {
} }
}; };
const testNewModelValueUnselectsAllOptions = (modelValue: any): void => { it('should reflect state of model after option selected and new options subsequently added',
setSelectedCities([comp.cities[1]]); fakeAsync(() => {
setSelectedCities([]);
selectOptionViaUI('1: Object');
assertOptionElementSelectedState([false, true, false]); assertOptionElementSelectedState([false, true, false]);
setSelectedCities(modelValue); comp.cities.push({'name': 'Chicago'});
detectChangesAndTick();
const select = fixture.debugElement.query(By.css('select')); assertOptionElementSelectedState([false, true, false, false]);
expect(select.nativeElement.value).toEqual('');
assertOptionElementSelectedState([false, false, false]);
};
it('should support setting value to null and undefined', fakeAsync(() => {
testNewModelValueUnselectsAllOptions(null);
testNewModelValueUnselectsAllOptions(undefined);
})); }));
it('should clear all selected option elements when value of wrong type supplied', it('should reflect state of model after option selected and then other options removed',
fakeAsync(() => { testNewModelValueUnselectsAllOptions(''); })); fakeAsync(() => {
setSelectedCities([]);
it('should set option elements to selected that are present in model', fakeAsync(() => { selectOptionViaUI('1: Object');
setSelectedCities([comp.cities[0], comp.cities[2]]);
assertOptionElementSelectedState([true, false, true]);
}));
it('should clear selected option elements since removed from model', fakeAsync(() => {
const selectedCities: [{'name:string': string}] = <[{'name:string': string}]>[];
selectedCities.push.apply(selectedCities, comp.cities);
setSelectedCities(selectedCities);
setSelectedCities([comp.cities[1]]);
assertOptionElementSelectedState([false, true, false]); assertOptionElementSelectedState([false, true, false]);
comp.cities.pop();
detectChangesAndTick();
assertOptionElementSelectedState([false, true]);
})); }));
}); });