mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
tinayuangao
parent
3569805a4a
commit
13524c14d1
@@ -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>
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user