fix(select): allow for null values in HTML select options bound with ngValue
closes #12829
This commit is contained in:
parent
b55aaf094f
commit
46023e4792
|
@ -6,13 +6,13 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Renderer, forwardRef} from '@angular/core';
|
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
|
||||||
|
|
||||||
import {isPrimitive, looseIdentical} from '../facade/lang';
|
import {isPrimitive, looseIdentical} from '../facade/lang';
|
||||||
|
|
||||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
||||||
|
|
||||||
export const SELECT_VALUE_ACCESSOR: any = {
|
export const SELECT_VALUE_ACCESSOR: Provider = {
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
useExisting: forwardRef(() => SelectControlValueAccessor),
|
useExisting: forwardRef(() => SelectControlValueAccessor),
|
||||||
multi: true
|
multi: true
|
||||||
|
@ -115,8 +115,8 @@ export class SelectControlValueAccessor implements ControlValueAccessor {
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_getOptionValue(valueString: string): any {
|
_getOptionValue(valueString: string): any {
|
||||||
let key: string = _extractId(valueString);
|
const id: string = _extractId(valueString);
|
||||||
return this._optionMap.has(key) ? this._optionMap.get(key) : valueString;
|
return this._optionMap.has(id) ? this._optionMap.get(id) : valueString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +158,7 @@ export class NgSelectOption implements OnDestroy {
|
||||||
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
|
this._renderer.setElementProperty(this._element.nativeElement, 'value', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
if (this._select) {
|
if (this._select) {
|
||||||
this._select._optionMap.delete(this.id);
|
this._select._optionMap.delete(this.id);
|
||||||
this._select.writeValue(this._select.value);
|
this._select.writeValue(this._select.value);
|
||||||
|
|
|
@ -6,13 +6,13 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Directive, ElementRef, Host, Input, OnDestroy, OpaqueToken, Optional, Renderer, Type, forwardRef} from '@angular/core';
|
import {Directive, ElementRef, Host, Input, OnDestroy, Optional, Provider, Renderer, forwardRef} from '@angular/core';
|
||||||
|
|
||||||
import {isPrimitive, looseIdentical} from '../facade/lang';
|
import {isPrimitive, looseIdentical} from '../facade/lang';
|
||||||
|
|
||||||
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from './control_value_accessor';
|
||||||
|
|
||||||
export const SELECT_MULTIPLE_VALUE_ACCESSOR = {
|
export const SELECT_MULTIPLE_VALUE_ACCESSOR: Provider = {
|
||||||
provide: NG_VALUE_ACCESSOR,
|
provide: NG_VALUE_ACCESSOR,
|
||||||
useExisting: forwardRef(() => SelectMultipleControlValueAccessor),
|
useExisting: forwardRef(() => SelectMultipleControlValueAccessor),
|
||||||
multi: true
|
multi: true
|
||||||
|
@ -121,8 +121,8 @@ export class SelectMultipleControlValueAccessor implements ControlValueAccessor
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_getOptionValue(valueString: string): any {
|
_getOptionValue(valueString: string): any {
|
||||||
let key: string = _extractId(valueString);
|
const id: string = _extractId(valueString);
|
||||||
return this._optionMap.has(key) ? this._optionMap.get(key)._value : valueString;
|
return this._optionMap.has(id) ? this._optionMap.get(id)._value : valueString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -180,12 +180,10 @@ export class NgSelectMultipleOption implements OnDestroy {
|
||||||
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
|
this._renderer.setElementProperty(this._element.nativeElement, 'selected', selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
ngOnDestroy(): void {
|
||||||
if (this._select) {
|
if (this._select) {
|
||||||
this._select._optionMap.delete(this.id);
|
this._select._optionMap.delete(this.id);
|
||||||
this._select.writeValue(this._select.value);
|
this._select.writeValue(this._select.value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SELECT_DIRECTIVES = [SelectMultipleControlValueAccessor, NgSelectMultipleOption];
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ export function main() {
|
||||||
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
|
NgModelRadioForm, NgModelRangeForm, NgModelSelectForm, NgNoFormComp, InvalidNgModelNoName,
|
||||||
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
|
NgModelOptionsStandalone, NgModelCustomComp, NgModelCustomWrapper,
|
||||||
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
|
NgModelValidationBindings, NgModelMultipleValidators, NgAsyncValidator,
|
||||||
NgModelAsyncValidation
|
NgModelAsyncValidation, NgModelSelectWithNullForm
|
||||||
],
|
],
|
||||||
imports: [FormsModule]
|
imports: [FormsModule]
|
||||||
});
|
});
|
||||||
|
@ -699,6 +699,28 @@ export function main() {
|
||||||
expect(select.nativeElement.value).toEqual('2: Object');
|
expect(select.nativeElement.value).toEqual('2: Object');
|
||||||
expect(secondNYC.nativeElement.selected).toBe(true);
|
expect(secondNYC.nativeElement.selected).toBe(true);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
it('should work with null option', fakeAsync(() => {
|
||||||
|
const fixture = TestBed.createComponent(NgModelSelectWithNullForm);
|
||||||
|
const comp = fixture.componentInstance;
|
||||||
|
comp.cities = [{'name': 'SF'}, {'name': 'NYC'}];
|
||||||
|
comp.selectedCity = null;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const select = fixture.debugElement.query(By.css('select'));
|
||||||
|
|
||||||
|
select.nativeElement.value = '2: Object';
|
||||||
|
dispatchEvent(select.nativeElement, 'change');
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
expect(comp.selectedCity['name']).toEqual('NYC');
|
||||||
|
|
||||||
|
select.nativeElement.value = '0: null';
|
||||||
|
dispatchEvent(select.nativeElement, 'change');
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
expect(comp.selectedCity).toEqual(null);
|
||||||
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('custom value accessors', () => {
|
describe('custom value accessors', () => {
|
||||||
|
@ -1078,6 +1100,20 @@ class NgModelSelectForm {
|
||||||
cities: any[] = [];
|
cities: any[] = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ng-model-select-null-form',
|
||||||
|
template: `
|
||||||
|
<select [(ngModel)]="selectedCity">
|
||||||
|
<option *ngFor="let c of cities" [ngValue]="c"> {{c.name}} </option>
|
||||||
|
<option [ngValue]="null">Unspecified</option>
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
})
|
||||||
|
class NgModelSelectWithNullForm {
|
||||||
|
selectedCity: {[k: string]: string} = {};
|
||||||
|
cities: any[] = [];
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ng-model-custom-comp',
|
selector: 'ng-model-custom-comp',
|
||||||
template: `
|
template: `
|
||||||
|
|
Loading…
Reference in New Issue