fix(forms): provide a method to compare options (#13349)
Closes #13268 PR Close #13349
This commit is contained in:
parent
6c7300c7de
commit
f89d004c51
@ -57,6 +57,31 @@ function _extractId(valueString: string): string {
|
|||||||
*
|
*
|
||||||
* {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
|
* {@example forms/ts/reactiveSelectControl/reactive_select_control_example.ts region='Component'}
|
||||||
*
|
*
|
||||||
|
* ### Caveat: Option selection
|
||||||
|
*
|
||||||
|
* Angular uses object identity to select option. It's possible for the identities of items
|
||||||
|
* to change while the data does not. This can happen, for example, if the items are produced
|
||||||
|
* from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the
|
||||||
|
* second response will produce objects with different identities.
|
||||||
|
*
|
||||||
|
* To customize the default option comparison algorithm, `<select>` supports `compareWith` input.
|
||||||
|
* `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
|
||||||
|
* If `compareWith` is given, Angular selects option by the return value of the function.
|
||||||
|
*
|
||||||
|
* #### Syntax
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* <select [compareWith]="compareFn" [(ngModel)]="selectedCountries">
|
||||||
|
* <option *ngFor="let country of countries" [ngValue]="country">
|
||||||
|
* {{country.name}}
|
||||||
|
* </option>
|
||||||
|
* </select>
|
||||||
|
*
|
||||||
|
* compareFn(c1: Country, c2: Country): boolean {
|
||||||
|
* return c1 && c2 ? c1.id === c2.id : c1 === c2;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* Note: We listen to the 'change' event because 'input' events aren't fired
|
* Note: We listen to the 'change' event because 'input' events aren't fired
|
||||||
* for selects in Firefox and IE:
|
* for selects in Firefox and IE:
|
||||||
* https://bugzilla.mozilla.org/show_bug.cgi?id=1024350
|
* https://bugzilla.mozilla.org/show_bug.cgi?id=1024350
|
||||||
@ -82,6 +107,16 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
|
|||||||
onChange = (_: any) => {};
|
onChange = (_: any) => {};
|
||||||
onTouched = () => {};
|
onTouched = () => {};
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set compareWith(fn: (o1: any, o2: any) => boolean) {
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
|
||||||
|
}
|
||||||
|
this._compareWith = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _compareWith: (o1: any, o2: any) => boolean = looseIdentical;
|
||||||
|
|
||||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||||
|
|
||||||
writeValue(value: any): void {
|
writeValue(value: any): void {
|
||||||
@ -112,7 +147,7 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_getOptionId(value: any): string {
|
_getOptionId(value: any): string {
|
||||||
for (const id of Array.from(this._optionMap.keys())) {
|
for (const id of Array.from(this._optionMap.keys())) {
|
||||||
if (looseIdentical(this._optionMap.get(id), value)) return id;
|
if (this._compareWith(this._optionMap.get(id), value)) return id;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -44,6 +44,31 @@ abstract class HTMLCollection {
|
|||||||
/**
|
/**
|
||||||
* The accessor for writing a value and listening to changes on a select element.
|
* The accessor for writing a value and listening to changes on a select element.
|
||||||
*
|
*
|
||||||
|
* ### Caveat: Options selection
|
||||||
|
*
|
||||||
|
* Angular uses object identity to select options. It's possible for the identities of items
|
||||||
|
* to change while the data does not. This can happen, for example, if the items are produced
|
||||||
|
* from an RPC to the server, and that RPC is re-run. Even if the data hasn't changed, the
|
||||||
|
* second response will produce objects with different identities.
|
||||||
|
*
|
||||||
|
* To customize the default option comparison algorithm, `<select multiple>` supports `compareWith`
|
||||||
|
* input. `compareWith` takes a **function** which has two arguments: `option1` and `option2`.
|
||||||
|
* If `compareWith` is given, Angular selects options by the return value of the function.
|
||||||
|
*
|
||||||
|
* #### Syntax
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* <select multiple [compareWith]="compareFn" [(ngModel)]="selectedCountries">
|
||||||
|
* <option *ngFor="let country of countries" [ngValue]="country">
|
||||||
|
* {{country.name}}
|
||||||
|
* </option>
|
||||||
|
* </select>
|
||||||
|
*
|
||||||
|
* compareFn(c1: Country, c2: Country): boolean {
|
||||||
|
* return c1 && c2 ? c1.id === c2.id : c1 === c2;
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
* @stable
|
* @stable
|
||||||
*/
|
*/
|
||||||
@Directive({
|
@Directive({
|
||||||
@ -62,6 +87,16 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
|
|||||||
onChange = (_: any) => {};
|
onChange = (_: any) => {};
|
||||||
onTouched = () => {};
|
onTouched = () => {};
|
||||||
|
|
||||||
|
@Input()
|
||||||
|
set compareWith(fn: (o1: any, o2: any) => boolean) {
|
||||||
|
if (typeof fn !== 'function') {
|
||||||
|
throw new Error(`compareWith must be a function, but received ${JSON.stringify(fn)}`);
|
||||||
|
}
|
||||||
|
this._compareWith = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _compareWith: (o1: any, o2: any) => boolean = looseIdentical;
|
||||||
|
|
||||||
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
constructor(private _renderer: Renderer, private _elementRef: ElementRef) {}
|
||||||
|
|
||||||
writeValue(value: any): void {
|
writeValue(value: any): void {
|
||||||
@ -119,7 +154,7 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
|
|||||||
/** @internal */
|
/** @internal */
|
||||||
_getOptionId(value: any): string {
|
_getOptionId(value: any): string {
|
||||||
for (const id of Array.from(this._optionMap.keys())) {
|
for (const id of Array.from(this._optionMap.keys())) {
|
||||||
if (looseIdentical(this._optionMap.get(id)._value, value)) return id;
|
if (this._compareWith(this._optionMap.get(id)._value, value)) return id;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -338,6 +338,95 @@ export function main() {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('select controls', () => {
|
||||||
|
it(`should support primitive values`, () => {
|
||||||
|
const fixture = initTest(FormControlNameSelect);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// model -> view
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('SF');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
|
||||||
|
select.nativeElement.value = 'NY';
|
||||||
|
dispatchEvent(select.nativeElement, 'change');
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// view -> model
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(false);
|
||||||
|
expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'});
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should support objects`, () => {
|
||||||
|
const fixture = initTest(FormControlSelectNgValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// model -> view
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if compareWith is not a function', () => {
|
||||||
|
const fixture = initTest(FormControlSelectWithCompareFn);
|
||||||
|
fixture.componentInstance.compareFn = null;
|
||||||
|
expect(() => fixture.detectChanges())
|
||||||
|
.toThrowError(/compareWith must be a function, but received null/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare options using provided compareWith function', () => {
|
||||||
|
const fixture = initTest(FormControlSelectWithCompareFn);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('select multiple controls', () => {
|
||||||
|
it('should support primitive values', () => {
|
||||||
|
const fixture = initTest(FormControlSelectMultiple);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual(`0: 'SF'`);
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support objects', () => {
|
||||||
|
const fixture = initTest(FormControlSelectMultipleNgValue);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error when compareWith is not a function', () => {
|
||||||
|
const fixture = initTest(FormControlSelectMultipleWithCompareFn);
|
||||||
|
fixture.componentInstance.compareFn = null;
|
||||||
|
expect(() => fixture.detectChanges())
|
||||||
|
.toThrowError(/compareWith must be a function, but received null/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare options using provided compareWith function', fakeAsync(() => {
|
||||||
|
const fixture = initTest(FormControlSelectMultipleWithCompareFn);
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
describe('form arrays', () => {
|
describe('form arrays', () => {
|
||||||
it('should support form arrays', () => {
|
it('should support form arrays', () => {
|
||||||
const fixture = initTest(FormArrayComp);
|
const fixture = initTest(FormArrayComp);
|
||||||
@ -835,25 +924,6 @@ export function main() {
|
|||||||
expect(control.value).toBe(false);
|
expect(control.value).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support <select>', () => {
|
|
||||||
const fixture = initTest(FormControlNameSelect);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
// model -> view
|
|
||||||
const select = fixture.debugElement.query(By.css('select'));
|
|
||||||
const sfOption = fixture.debugElement.query(By.css('option'));
|
|
||||||
expect(select.nativeElement.value).toEqual('SF');
|
|
||||||
expect(sfOption.nativeElement.selected).toBe(true);
|
|
||||||
|
|
||||||
select.nativeElement.value = 'NY';
|
|
||||||
dispatchEvent(select.nativeElement, 'change');
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
// view -> model
|
|
||||||
expect(sfOption.nativeElement.selected).toBe(false);
|
|
||||||
expect(fixture.componentInstance.form.value).toEqual({'city': 'NY'});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should support <type=number>', () => {
|
describe('should support <type=number>', () => {
|
||||||
it('with basic use case', () => {
|
it('with basic use case', () => {
|
||||||
const fixture = initTest(FormControlNumberInput);
|
const fixture = initTest(FormControlNumberInput);
|
||||||
@ -2005,6 +2075,82 @@ class FormControlNameSelect {
|
|||||||
form = new FormGroup({city: new FormControl('SF')});
|
form = new FormGroup({city: new FormControl('SF')});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-control-select-ngValue',
|
||||||
|
template: `
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<select formControlName="city">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c">{{c.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
class FormControlSelectNgValue {
|
||||||
|
cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}];
|
||||||
|
form = new FormGroup({city: new FormControl(this.cities[0])});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-control-select-compare-with',
|
||||||
|
template: `
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<select formControlName="city" [compareWith]="compareFn">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c">{{c.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
class FormControlSelectWithCompareFn {
|
||||||
|
compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => {
|
||||||
|
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||||
|
};
|
||||||
|
cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}];
|
||||||
|
form = new FormGroup({city: new FormControl({id: 1, name: 'SF'})});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-control-select-multiple',
|
||||||
|
template: `
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<select multiple formControlName="city">
|
||||||
|
<option *ngFor="let c of cities" [value]="c">{{c}}</option>
|
||||||
|
</select>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
class FormControlSelectMultiple {
|
||||||
|
cities = ['SF', 'NY'];
|
||||||
|
form = new FormGroup({city: new FormControl(['SF'])});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-control-select-multiple',
|
||||||
|
template: `
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<select multiple formControlName="city">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c">{{c.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
class FormControlSelectMultipleNgValue {
|
||||||
|
cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}];
|
||||||
|
form = new FormGroup({city: new FormControl([this.cities[0]])});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'form-control-select-multiple-compare-with',
|
||||||
|
template: `
|
||||||
|
<div [formGroup]="form">
|
||||||
|
<select multiple formControlName="city" [compareWith]="compareFn">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c">{{c.name}}</option>
|
||||||
|
</select>
|
||||||
|
</div>`
|
||||||
|
})
|
||||||
|
class FormControlSelectMultipleWithCompareFn {
|
||||||
|
compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => {
|
||||||
|
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||||
|
};
|
||||||
|
cities = [{id: 1, name: 'SF'}, {id: 2, name: 'NY'}];
|
||||||
|
form = new FormGroup({city: new FormControl([{id: 1, name: 'SF'}])});
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'wrapped-value-form',
|
selector: 'wrapped-value-form',
|
||||||
template: `
|
template: `
|
||||||
|
@ -764,69 +764,115 @@ export function main() {
|
|||||||
tick();
|
tick();
|
||||||
expect(comp.selectedCity).toEqual(null);
|
expect(comp.selectedCity).toEqual(null);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should throw an error when compareWith is not a function', () => {
|
||||||
|
const fixture = initTest(NgModelSelectWithCustomCompareFnForm);
|
||||||
|
const comp = fixture.componentInstance;
|
||||||
|
comp.compareFn = null;
|
||||||
|
expect(() => fixture.detectChanges())
|
||||||
|
.toThrowError(/compareWith must be a function, but received null/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should compare options using provided compareWith function', fakeAsync(() => {
|
||||||
|
const fixture = initTest(NgModelSelectWithCustomCompareFnForm);
|
||||||
|
const comp = fixture.componentInstance;
|
||||||
|
comp.selectedCity = {id: 1, name: 'SF'};
|
||||||
|
comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}];
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('select multiple controls', () => {
|
describe('select multiple controls', () => {
|
||||||
let fixture: ComponentFixture<NgModelSelectMultipleForm>;
|
describe('select options', () => {
|
||||||
let comp: NgModelSelectMultipleForm;
|
let fixture: ComponentFixture<NgModelSelectMultipleForm>;
|
||||||
|
let comp: NgModelSelectMultipleForm;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = initTest(NgModelSelectMultipleForm);
|
fixture = initTest(NgModelSelectMultipleForm);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
|
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}, {'name': 'Buffalo'}];
|
||||||
|
});
|
||||||
|
|
||||||
|
const detectChangesAndTick = (): void => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
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 options = fixture.debugElement.queryAll(By.css('option'));
|
||||||
|
if (options.length !== selectedStates.length) {
|
||||||
|
throw 'the selected state values to assert does not match the number of options';
|
||||||
|
}
|
||||||
|
for (let i = 0; i < selectedStates.length; i++) {
|
||||||
|
expect(options[i].nativeElement.selected).toBe(selectedStates[i]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should reflect state of model after option selected and new options subsequently added',
|
||||||
|
fakeAsync(() => {
|
||||||
|
setSelectedCities([]);
|
||||||
|
|
||||||
|
selectOptionViaUI('1: Object');
|
||||||
|
assertOptionElementSelectedState([false, true, false]);
|
||||||
|
|
||||||
|
comp.cities.push({'name': 'Chicago'});
|
||||||
|
detectChangesAndTick();
|
||||||
|
|
||||||
|
assertOptionElementSelectedState([false, true, false, false]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should reflect state of model after option selected and then other options removed',
|
||||||
|
fakeAsync(() => {
|
||||||
|
setSelectedCities([]);
|
||||||
|
|
||||||
|
selectOptionViaUI('1: Object');
|
||||||
|
assertOptionElementSelectedState([false, true, false]);
|
||||||
|
|
||||||
|
comp.cities.pop();
|
||||||
|
detectChangesAndTick();
|
||||||
|
|
||||||
|
assertOptionElementSelectedState([false, true]);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const detectChangesAndTick = (): void => {
|
it('should throw an error when compareWith is not a function', () => {
|
||||||
fixture.detectChanges();
|
const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm);
|
||||||
tick();
|
const comp = fixture.componentInstance;
|
||||||
};
|
comp.compareFn = null;
|
||||||
|
expect(() => fixture.detectChanges())
|
||||||
|
.toThrowError(/compareWith must be a function, but received null/);
|
||||||
|
});
|
||||||
|
|
||||||
const setSelectedCities = (selectedCities: any): void => {
|
it('should compare options using provided compareWith function', fakeAsync(() => {
|
||||||
comp.selectedCities = selectedCities;
|
const fixture = initTest(NgModelSelectMultipleWithCustomCompareFnForm);
|
||||||
detectChangesAndTick();
|
const comp = fixture.componentInstance;
|
||||||
};
|
comp.cities = [{id: 1, name: 'SF'}, {id: 2, name: 'LA'}];
|
||||||
|
comp.selectedCities = [comp.cities[0]];
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
|
||||||
const selectOptionViaUI = (valueString: string): void => {
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
const select = fixture.debugElement.query(By.css('select'));
|
const sfOption = fixture.debugElement.query(By.css('option'));
|
||||||
select.nativeElement.value = valueString;
|
expect(select.nativeElement.value).toEqual('0: Object');
|
||||||
dispatchEvent(select.nativeElement, 'change');
|
expect(sfOption.nativeElement.selected).toBe(true);
|
||||||
detectChangesAndTick();
|
|
||||||
};
|
|
||||||
|
|
||||||
const assertOptionElementSelectedState = (selectedStates: boolean[]): void => {
|
|
||||||
const options = fixture.debugElement.queryAll(By.css('option'));
|
|
||||||
if (options.length !== selectedStates.length) {
|
|
||||||
throw 'the selected state values to assert does not match the number of options';
|
|
||||||
}
|
|
||||||
for (let i = 0; i < selectedStates.length; i++) {
|
|
||||||
expect(options[i].nativeElement.selected).toBe(selectedStates[i]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should reflect state of model after option selected and new options subsequently added',
|
|
||||||
fakeAsync(() => {
|
|
||||||
setSelectedCities([]);
|
|
||||||
|
|
||||||
selectOptionViaUI('1: Object');
|
|
||||||
assertOptionElementSelectedState([false, true, false]);
|
|
||||||
|
|
||||||
comp.cities.push({'name': 'Chicago'});
|
|
||||||
detectChangesAndTick();
|
|
||||||
|
|
||||||
assertOptionElementSelectedState([false, true, false, false]);
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should reflect state of model after option selected and then other options removed',
|
|
||||||
fakeAsync(() => {
|
|
||||||
setSelectedCities([]);
|
|
||||||
|
|
||||||
selectOptionViaUI('1: Object');
|
|
||||||
assertOptionElementSelectedState([false, true, false]);
|
|
||||||
|
|
||||||
comp.cities.pop();
|
|
||||||
detectChangesAndTick();
|
|
||||||
|
|
||||||
assertOptionElementSelectedState([false, true]);
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1314,6 +1360,38 @@ class NgModelSelectWithNullForm {
|
|||||||
cities: any[] = [];
|
cities: any[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-model-select-compare-with',
|
||||||
|
template: `
|
||||||
|
<select [(ngModel)]="selectedCity" [compareWith]="compareFn">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class NgModelSelectWithCustomCompareFnForm {
|
||||||
|
compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => {
|
||||||
|
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||||
|
};
|
||||||
|
selectedCity: any = {};
|
||||||
|
cities: any[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-model-select-multiple-compare-with',
|
||||||
|
template: `
|
||||||
|
<select multiple [(ngModel)]="selectedCities" [compareWith]="compareFn">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class NgModelSelectMultipleWithCustomCompareFnForm {
|
||||||
|
compareFn: (o1: any, o2: any) => boolean = (o1: any, o2: any) => {
|
||||||
|
return o1 && o2 ? o1.id === o2.id : o1 === o2;
|
||||||
|
};
|
||||||
|
selectedCities: any[] = [];
|
||||||
|
cities: any[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ng-model-select-multiple-form',
|
selector: 'ng-model-select-multiple-form',
|
||||||
template: `
|
template: `
|
||||||
|
2
tools/public_api_guard/forms/index.d.ts
vendored
2
tools/public_api_guard/forms/index.d.ts
vendored
@ -499,6 +499,7 @@ export declare class RequiredValidator implements Validator {
|
|||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare class SelectControlValueAccessor implements ControlValueAccessor {
|
export declare class SelectControlValueAccessor implements ControlValueAccessor {
|
||||||
|
compareWith: (o1: any, o2: any) => boolean;
|
||||||
onChange: (_: any) => void;
|
onChange: (_: any) => void;
|
||||||
onTouched: () => void;
|
onTouched: () => void;
|
||||||
value: any;
|
value: any;
|
||||||
@ -511,6 +512,7 @@ export declare class SelectControlValueAccessor implements ControlValueAccessor
|
|||||||
|
|
||||||
/** @stable */
|
/** @stable */
|
||||||
export declare class SelectMultipleControlValueAccessor implements ControlValueAccessor {
|
export declare class SelectMultipleControlValueAccessor implements ControlValueAccessor {
|
||||||
|
compareWith: (o1: any, o2: any) => boolean;
|
||||||
onChange: (_: any) => void;
|
onChange: (_: any) => void;
|
||||||
onTouched: () => void;
|
onTouched: () => void;
|
||||||
value: any;
|
value: any;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user