diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index 7b9c87a3ccfe..6175ceae5efb 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -4,8 +4,9 @@ ngModel - + + None {{ drink.viewValue }} diff --git a/src/demo-app/select/select-demo.ts b/src/demo-app/select/select-demo.ts index 6e2ff635a202..089fd9a4eb31 100644 --- a/src/demo-app/select/select-demo.ts +++ b/src/demo-app/select/select-demo.ts @@ -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'} diff --git a/src/lib/select/select.md b/src/lib/select/select.md index 3cce49f7397d..11098c03b26d 100644 --- a/src/lib/select/select.md +++ b/src/lib/select/select.md @@ -1,13 +1,13 @@ `` is a form control for selecting a value from a set of options, similar to the native -`` element. You can read more about selects in the [Material Design spec](https://material.google.com/components/menus.html). ### 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 + + None + {{ state.name }} + +``` + ### 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 {{ state.name }} -``` +``` #### Keyboard interaction: - DOWN_ARROW: Focus next option diff --git a/src/lib/select/select.spec.ts b/src/lib/select/select.spec.ts index e67335d2e56a..b65c0cc0aedc 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -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; + let trigger: HTMLElement; + let placeholder: HTMLElement; + let options: NodeListOf; + + 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; + + 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: ` + + + {{ food.viewValue }} + + + None + + ` +}) +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; +} diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 273008146ea2..3bcf2240c03f 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -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));