Skip to content

Commit 13524c1

Browse files
crisbetotinayuangao
authored andcommitted
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
1 parent 3569805 commit 13524c1

File tree

5 files changed

+143
-16
lines changed

5 files changed

+143
-16
lines changed

src/demo-app/select/select-demo.html

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
<md-card>
55
<md-card-subtitle>ngModel</md-card-subtitle>
66

7-
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
8-
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
7+
<md-select placeholder="Drink" [color]="drinksTheme" [(ngModel)]="currentDrink" [required]="drinksRequired"
8+
[disabled]="drinksDisabled" [floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
9+
<md-option>None</md-option>
910
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
1011
{{ drink.viewValue }}
1112
</md-option>

src/demo-app/select/select-demo.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ export class SelectDemo {
2323
pokemonTheme = 'primary';
2424

2525
foods = [
26+
{value: null, viewValue: 'None'},
2627
{value: 'steak-0', viewValue: 'Steak'},
2728
{value: 'pizza-1', viewValue: 'Pizza'},
2829
{value: 'tacos-2', viewValue: 'Tacos'}

src/lib/select/select.md

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
`<md-select>` is a form control for selecting a value from a set of options, similar to the native
2-
`<select>` element. You can read more about selects in the
2+
`<select>` element. You can read more about selects in the
33
[Material Design spec](https://material.google.com/components/menus.html).
44

55
<!-- example(select-overview) -->
66

77
### Simple select
88

9-
In your template, create an `md-select` element. For each option you'd like in your select, add an
10-
`md-option` tag. Note that you can disable items by adding the `disabled` boolean attribute or
9+
In your template, create an `md-select` element. For each option you'd like in your select, add an
10+
`md-option` tag. Note that you can disable items by adding the `disabled` boolean attribute or
1111
binding to it.
1212

1313
*my-comp.html*
@@ -19,7 +19,7 @@ binding to it.
1919

2020
### Getting and setting the select value
2121

22-
The select component is set up as a custom value accessor, so you can manipulate the select's value using
22+
The select component is set up as a custom value accessor, so you can manipulate the select's value using
2323
any of the form directives from the core `FormsModule` or `ReactiveFormsModule`: `ngModel`, `formControl`, etc.
2424

2525
*my-comp.html*
@@ -37,18 +37,30 @@ class MyComp {
3737
}
3838
```
3939

40+
### Resetting the select value
41+
42+
If you want one of your options to reset the select's value, you can omit specifying its value:
43+
44+
*my-comp.html*
45+
```html
46+
<md-select placeholder="State">
47+
<md-option>None</md-option>
48+
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
49+
</md-select>
50+
```
51+
4052
### Setting a static placeholder
4153

4254
It's possible to turn off the placeholder's floating animation using the `floatPlaceholder` property. It accepts one of three string options:
4355
- `'auto'`: This is the default floating placeholder animation. It will float up when a selection is made.
4456
- `'never'`: This makes the placeholder static. Rather than floating, it will disappear once a selection is made.
4557
- `'always'`: This makes the placeholder permanently float above the input. It will not animate up or down.
46-
58+
4759
```html
4860
<md-select placeholder="State" [(ngModel)]="myState" floatPlaceholder="never">
4961
<md-option *ngFor="let state of states" [value]="state.code">{{ state.name }}</md-option>
5062
</md-select>
51-
```
63+
```
5264

5365
#### Keyboard interaction:
5466
- <kbd>DOWN_ARROW</kbd>: Focus next option

src/lib/select/select.spec.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ describe('MdSelect', () => {
5555
SelectEarlyAccessSibling,
5656
BasicSelectInitiallyHidden,
5757
BasicSelectNoPlaceholder,
58-
BasicSelectWithTheming
58+
BasicSelectWithTheming,
59+
ResetValuesSelect
5960
],
6061
providers: [
6162
{provide: OverlayContainer, useFactory: () => {
@@ -2022,6 +2023,82 @@ describe('MdSelect', () => {
20222023

20232024
});
20242025

2026+
2027+
describe('reset values', () => {
2028+
let fixture: ComponentFixture<ResetValuesSelect>;
2029+
let trigger: HTMLElement;
2030+
let placeholder: HTMLElement;
2031+
let options: NodeListOf<HTMLElement>;
2032+
2033+
beforeEach(() => {
2034+
fixture = TestBed.createComponent(ResetValuesSelect);
2035+
fixture.detectChanges();
2036+
trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
2037+
placeholder = fixture.debugElement.query(By.css('.mat-select-placeholder')).nativeElement;
2038+
2039+
trigger.click();
2040+
fixture.detectChanges();
2041+
options = overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
2042+
2043+
options[0].click();
2044+
fixture.detectChanges();
2045+
});
2046+
2047+
it('should reset when an option with an undefined value is selected', () => {
2048+
options[4].click();
2049+
fixture.detectChanges();
2050+
2051+
expect(fixture.componentInstance.control.value).toBeUndefined();
2052+
expect(fixture.componentInstance.select.selected).toBeFalsy();
2053+
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
2054+
expect(trigger.textContent).not.toContain('Undefined');
2055+
});
2056+
2057+
it('should reset when an option with a null value is selected', () => {
2058+
options[5].click();
2059+
fixture.detectChanges();
2060+
2061+
expect(fixture.componentInstance.control.value).toBeNull();
2062+
expect(fixture.componentInstance.select.selected).toBeFalsy();
2063+
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
2064+
expect(trigger.textContent).not.toContain('Null');
2065+
});
2066+
2067+
it('should reset when a blank option is selected', () => {
2068+
options[6].click();
2069+
fixture.detectChanges();
2070+
2071+
expect(fixture.componentInstance.control.value).toBeUndefined();
2072+
expect(fixture.componentInstance.select.selected).toBeFalsy();
2073+
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
2074+
expect(trigger.textContent).not.toContain('None');
2075+
});
2076+
2077+
it('should not reset when any other falsy option is selected', () => {
2078+
options[3].click();
2079+
fixture.detectChanges();
2080+
2081+
expect(fixture.componentInstance.control.value).toBe(false);
2082+
expect(fixture.componentInstance.select.selected).toBeTruthy();
2083+
expect(placeholder.classList).toContain('mat-floating-placeholder');
2084+
expect(trigger.textContent).toContain('Falsy');
2085+
});
2086+
2087+
it('should not consider the reset values as selected when resetting the form control', () => {
2088+
expect(placeholder.classList).toContain('mat-floating-placeholder');
2089+
2090+
fixture.componentInstance.control.reset();
2091+
fixture.detectChanges();
2092+
2093+
expect(fixture.componentInstance.control.value).toBeNull();
2094+
expect(fixture.componentInstance.select.selected).toBeFalsy();
2095+
expect(placeholder.classList).not.toContain('mat-floating-placeholder');
2096+
expect(trigger.textContent).not.toContain('Null');
2097+
expect(trigger.textContent).not.toContain('Undefined');
2098+
});
2099+
2100+
});
2101+
20252102
});
20262103

20272104

@@ -2366,3 +2443,29 @@ class BasicSelectWithTheming {
23662443
@ViewChild(MdSelect) select: MdSelect;
23672444
theme: string;
23682445
}
2446+
2447+
@Component({
2448+
selector: 'reset-values-select',
2449+
template: `
2450+
<md-select placeholder="Food" [formControl]="control">
2451+
<md-option *ngFor="let food of foods" [value]="food.value">
2452+
{{ food.viewValue }}
2453+
</md-option>
2454+
2455+
<md-option>None</md-option>
2456+
</md-select>
2457+
`
2458+
})
2459+
class ResetValuesSelect {
2460+
foods: any[] = [
2461+
{ value: 'steak-0', viewValue: 'Steak' },
2462+
{ value: 'pizza-1', viewValue: 'Pizza' },
2463+
{ value: 'tacos-2', viewValue: 'Tacos' },
2464+
{ value: false, viewValue: 'Falsy' },
2465+
{ viewValue: 'Undefined' },
2466+
{ value: null, viewValue: 'Null' },
2467+
];
2468+
control = new FormControl();
2469+
2470+
@ViewChild(MdSelect) select: MdSelect;
2471+
}

src/lib/select/select.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -578,7 +578,7 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
578578
*/
579579
private _selectValue(value: any): MdOption {
580580
let optionsArray = this.options.toArray();
581-
let correspondingOption = optionsArray.find(option => option.value === value);
581+
let correspondingOption = optionsArray.find(option => option.value && option.value === value);
582582

583583
if (correspondingOption) {
584584
correspondingOption.select();
@@ -638,13 +638,19 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
638638
private _onSelect(option: MdOption): void {
639639
const wasSelected = this._selectionModel.isSelected(option);
640640

641+
// TODO(crisbeto): handle blank/null options inside multi-select.
641642
if (this.multiple) {
642643
this._selectionModel.toggle(option);
643644
wasSelected ? option.deselect() : option.select();
644645
this._sortValues();
645646
} else {
646-
this._clearSelection(option);
647-
this._selectionModel.select(option);
647+
this._clearSelection(option.value == null ? null : option);
648+
649+
if (option.value == null) {
650+
this._propagateChanges(option.value);
651+
} else {
652+
this._selectionModel.select(option);
653+
}
648654
}
649655

650656
if (wasSelected !== this._selectionModel.isSelected(option)) {
@@ -677,10 +683,14 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal
677683
}
678684

679685
/** Emits change event to set the model value. */
680-
private _propagateChanges(): void {
681-
let valueToEmit = Array.isArray(this.selected) ?
682-
this.selected.map(option => option.value) :
683-
this.selected.value;
686+
private _propagateChanges(fallbackValue?: any): void {
687+
let valueToEmit = null;
688+
689+
if (Array.isArray(this.selected)) {
690+
valueToEmit = this.selected.map(option => option.value);
691+
} else {
692+
valueToEmit = this.selected ? this.selected.value : fallbackValue;
693+
}
684694

685695
this._onChange(valueToEmit);
686696
this.change.emit(new MdSelectChange(this, valueToEmit));

0 commit comments

Comments
 (0)