fix(select): allow option with undefined or null value to clear selection (#3141)

* fix(select): allow option with undefined or null value to clear selection

Allows for options, with a value of `null` or `undefined`, to clear the select. This is similar to the way the native select works.

Fixes #3110.
Fixes #2634.

* fix: address feedback
This commit is contained in:
Kristiyan Kostadinov
2017-05-23 22:54:37 +02:00
committed by tinayuangao
parent 3569805a4a
commit 13524c14d1
5 changed files with 143 additions and 16 deletions
+3 -2
View File
@@ -4,8 +4,9 @@
<md-card>
<md-card-subtitle>ngModel</md-card-subtitle>
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired"
[disabled]="drinksDisabled" [floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
<md-option>None</md-option>
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
</md-option>
+1
View File
@@ -23,6 +23,7 @@ export class SelectDemo {
pokemonTheme = 'primary';
foods = [
{value: null, viewValue: 'None'},
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'}
+18 -6
View File
@@ -1,13 +1,13 @@
`<md-select>` is a form control for selecting a value from a set of options, similar to the native
`<select>` element. You can read more about selects in the
`<select>` element. You can read more about selects in the
[Material Design spec](https://material.google.com/components/menus.html).
<!-- example(select-overview) -->
### Simple select
In your template, create an `md-select` element. For each option you'd like in your select, add an
`md-option` tag. Note that you can disable items by adding the `disabled` boolean attribute or
In your template, create an `md-select` element. For each option you'd like in your select, add an
`md-option` tag. Note that you can disable items by adding the `disabled` boolean attribute or
binding to it.
*my-comp.html*
@@ -19,7 +19,7 @@ binding to it.
### Getting and setting the select value
The select component is set up as a custom value accessor, so you can manipulate the select's value using
The select component is set up as a custom value accessor, so you can manipulate the select's value using
any of the form directives from the core `FormsModule` or `ReactiveFormsModule`: `ngModel`, `formControl`, etc.
*my-comp.html*
@@ -37,18 +37,30 @@ class MyComp {
}
```
### Resetting the select value
If you want one of your options to reset the select's value, you can omit specifying its value:
*my-comp.html*
```html
<md-select placeholder="State">
<md-option>None</md-option>
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
```
### Setting a static placeholder
It's possible to turn off the placeholder's floating animation using the `floatPlaceholder` property. It accepts one of three string options:
- `'auto'`: This is the default floating placeholder animation. It will float up when a selection is made.
- `'never'`: This makes the placeholder static. Rather than floating, it will disappear once a selection is made.
- `'always'`: This makes the placeholder permanently float above the input. It will not animate up or down.
```html
<md-select placeholder="State" [(ngModel)]="myState" floatPlaceholder="never">
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
</md-select>
```
```
#### Keyboard interaction:
- <kbd>DOWN_ARROW</kbd>: Focus next option
+104 -1
View File
@@ -55,7 +55,8 @@ describe('MdSelect', () => {
SelectEarlyAccessSibling,
BasicSelectInitiallyHidden,
BasicSelectNoPlaceholder,
BasicSelectWithTheming
BasicSelectWithTheming,
ResetValuesSelect
],
providers: [
{provide: OverlayContainer, useFactory: () => {
@@ -2022,6 +2023,82 @@ describe('MdSelect', () => {
});
describe('reset values', () => {
let fixture: ComponentFixture<ResetValuesSelect>;
let trigger: HTMLElement;
let placeholder: HTMLElement;
let options: NodeListOf<HTMLElement>;
beforeEach(() => {
fixture = TestBed.createComponent(ResetValuesSelect);
fixture.detectChanges();
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
placeholder = fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
trigger.click();
fixture.detectChanges();
options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
options[0].click();
fixture.detectChanges();
});
it('should reset when an option with an undefined value is selected', () => {
options[4].click();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
expect(trigger.textContent).not.toContain('Undefined');
});
it('should reset when an option with a null value is selected', () => {
options[5].click();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
expect(trigger.textContent).not.toContain('Null');
});
it('should reset when a blank option is selected', () => {
options[6].click();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeUndefined();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
expect(trigger.textContent).not.toContain('None');
});
it('should not reset when any other falsy option is selected', () => {
options[3].click();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBe(false);
expect(fixture.componentInstance.select.selected).toBeTruthy();
expect(placeholder.classList).toContain('mat-floating-placeholder');
expect(trigger.textContent).toContain('Falsy');
});
it('should not consider the reset values as selected when resetting the form control', () => {
expect(placeholder.classList).toContain('mat-floating-placeholder');
fixture.componentInstance.control.reset();
fixture.detectChanges();
expect(fixture.componentInstance.control.value).toBeNull();
expect(fixture.componentInstance.select.selected).toBeFalsy();
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
expect(trigger.textContent).not.toContain('Null');
expect(trigger.textContent).not.toContain('Undefined');
});
});
});
@@ -2366,3 +2443,29 @@ class BasicSelectWithTheming {
@ViewChild(MdSelect) select: MdSelect;
theme: string;
}
@Component({
selector: 'reset-values-select',
template: `
<md-select placeholder="Food" [formControl]="control">
<md-option *ngFor="let food of foods" [value]="food.value">
{{ food.viewValue }}
</md-option>
<md-option>None</md-option>
</md-select>
`
})
class ResetValuesSelect {
foods: any[] = [
{ value: 'steak-0', viewValue: 'Steak' },
{ value: 'pizza-1', viewValue: 'Pizza' },
{ value: 'tacos-2', viewValue: 'Tacos' },
{ value: false, viewValue: 'Falsy' },
{ viewValue: 'Undefined' },
{ value: null, viewValue: 'Null' },
];
control = new FormControl();
@ViewChild(MdSelect) select: MdSelect;
}
+17 -7
View File
@@ -578,7 +578,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
*/
private _selectValue(value: any): MdOption {
let optionsArray = this.options.toArray();
let correspondingOption = optionsArray.find(option => option.value === value);
let correspondingOption = optionsArray.find(option => option.value && option.value === value);
if (correspondingOption) {
correspondingOption.select();
@@ -638,13 +638,19 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
private _onSelect(option: MdOption): void {
const wasSelected = this._selectionModel.isSelected(option);
// TODO(crisbeto): handle blank/null options inside multi-select.
if (this.multiple) {
this._selectionModel.toggle(option);
wasSelected ? option.deselect() : option.select();
this._sortValues();
} else {
this._clearSelection(option);
this._selectionModel.select(option);
this._clearSelection(option.value == null ? null : option);
if (option.value == null) {
this._propagateChanges(option.value);
} else {
this._selectionModel.select(option);
}
}
if (wasSelected !== this._selectionModel.isSelected(option)) {
@@ -677,10 +683,14 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
}
/** Emits change event to set the model value. */
private _propagateChanges(): void {
let valueToEmit = Array.isArray(this.selected) ?
this.selected.map(option => option.value) :
this.selected.value;
private _propagateChanges(fallbackValue?: any): void {
let valueToEmit = null;
if (Array.isArray(this.selected)) {
valueToEmit = this.selected.map(option => option.value);
} else {
valueToEmit = this.selected ? this.selected.value : fallbackValue;
}
this._onChange(valueToEmit);
this.change.emit(new MdSelectChange(this, valueToEmit));