Skip to content

Commit

Permalink
fix(select): allow option with undefined or null value to clear selec…
Browse files Browse the repository at this point in the history
…tion (#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
  • Loading branch information
crisbeto authored and tinayuangao committed May 23, 2017
1 parent 3569805 commit 13524c1
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 16 deletions.
5 changes: 3 additions & 2 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
1 change: 1 addition & 0 deletions src/demo-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'}
Expand Down
24 changes: 18 additions & 6 deletions src/lib/select/select.md
Original file line number Diff line number Diff line change
@@ -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*
Expand All @@ -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*
Expand All @@ -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
Expand Down
105 changes: 104 additions & 1 deletion src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ describe('MdSelect', () => {
SelectEarlyAccessSibling,
BasicSelectInitiallyHidden,
BasicSelectNoPlaceholder,
BasicSelectWithTheming
BasicSelectWithTheming,
ResetValuesSelect
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -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');
});

});

});


Expand Down Expand Up @@ -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;
}
24 changes: 17 additions & 7 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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));
Expand Down

1 comment on commit 13524c1

@niveo
Copy link

@niveo niveo commented on 13524c1 May 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.