From 7b47083b1b33e8d835fc6c95fd1904acc1942022 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Thu, 16 Feb 2017 22:41:15 +0100 Subject: [PATCH 1/2] 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. --- src/demo-app/select/select-demo.html | 2 + src/demo-app/select/select-demo.ts | 1 + src/lib/select/select.spec.ts | 95 +++++++++++++++++++++++++++- src/lib/select/select.ts | 12 +++- 4 files changed, 106 insertions(+), 4 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index 7b9c87a3ccfe..904cc52d4ecc 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -6,6 +6,8 @@ + #drinkControl="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.spec.ts b/src/lib/select/select.spec.ts index 43a4af6673cf..141f2fdb7b14 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -56,7 +56,8 @@ describe('MdSelect', () => { SelectEarlyAccessSibling, BasicSelectInitiallyHidden, BasicSelectNoPlaceholder, - BasicSelectWithTheming + BasicSelectWithTheming, + ResetValuesSelect ], providers: [ {provide: OverlayContainer, useFactory: () => { @@ -1995,6 +1996,72 @@ 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 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'); + }); + + }); + }); @@ -2339,3 +2406,29 @@ class BasicSelectWithTheming { @ViewChild(MdSelect) select: MdSelect; theme: string; } + +@Component({ + selector: 'reset-values-select', + template: ` + + + {{ food.viewValue }} + + + ` +}) +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(); + isRequired: boolean; + + @ViewChild(MdSelect) select: MdSelect; + @ViewChildren(MdOption) options: QueryList; +} diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 5099709a42f6..0007df4d0b97 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -567,7 +567,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(); @@ -632,8 +632,14 @@ export class MdSelect implements AfterContentInit, OnDestroy, OnInit, ControlVal wasSelected ? option.deselect() : option.select(); this._sortValues(); } else { - this._clearSelection(option); - this._selectionModel.select(option); + if (option.value == null) { + this._clearSelection(); + this._onChange(option.value); + this.change.emit(new MdSelectChange(this, option.value)); + } else { + this._clearSelection(option); + this._selectionModel.select(option); + } } if (wasSelected !== this._selectionModel.isSelected(option)) { From c6fce3729674ec9839cf0b2d177ce7a1fc202bf6 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Mon, 22 May 2017 21:22:30 +0200 Subject: [PATCH 2/2] fix: address feedback --- src/demo-app/select/select-demo.html | 5 ++--- src/lib/select/select.md | 24 ++++++++++++++++++------ src/lib/select/select.spec.ts | 16 +++++++++++++--- src/lib/select/select.ts | 20 ++++++++++++-------- 4 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/demo-app/select/select-demo.html b/src/demo-app/select/select-demo.html index 904cc52d4ecc..6175ceae5efb 100644 --- a/src/demo-app/select/select-demo.html +++ b/src/demo-app/select/select-demo.html @@ -4,9 +4,8 @@ ngModel - - #drinkControl="ngModel"> + None {{ drink.viewValue }} 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 141f2fdb7b14..4bc1038a22f3 100644 --- a/src/lib/select/select.spec.ts +++ b/src/lib/select/select.spec.ts @@ -2037,6 +2037,16 @@ describe('MdSelect', () => { 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(); @@ -2410,10 +2420,12 @@ class BasicSelectWithTheming { @Component({ selector: 'reset-values-select', template: ` - + {{ food.viewValue }} + + None ` }) @@ -2427,8 +2439,6 @@ class ResetValuesSelect { { value: null, viewValue: 'Null' }, ]; control = new FormControl(); - isRequired: boolean; @ViewChild(MdSelect) select: MdSelect; - @ViewChildren(MdOption) options: QueryList; } diff --git a/src/lib/select/select.ts b/src/lib/select/select.ts index 0007df4d0b97..59b46e74fa3a 100644 --- a/src/lib/select/select.ts +++ b/src/lib/select/select.ts @@ -627,17 +627,17 @@ 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.value == null ? null : option); + if (option.value == null) { - this._clearSelection(); - this._onChange(option.value); - this.change.emit(new MdSelectChange(this, option.value)); + this._propagateChanges(option.value); } else { - this._clearSelection(option); this._selectionModel.select(option); } } @@ -672,10 +672,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));