mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(select): support basic usage without @angular/forms (#5871)
* feat(select): support basic usage without @angular/forms Currently `md-select` can only really be used together with `@angular/forms` which is overkill for simple usages where it only sets a value (for example, the only reason the paginator module brings in the `FormsModule` is the select). These changes introduce the `value` two-way binding that can be used to read/write the value without using `ngModel` or a `formControl`. This also aligns it with the input module. Relates to #5717. * chore: add demo
This commit is contained in:
committed by
Andrew Seguin
parent
ebb5e9e23f
commit
9a90eaf97d
@@ -65,6 +65,22 @@ Space above cards: <input type="number" [formControl]="topHeightCtrl">
|
||||
</md-card-content>
|
||||
</md-card>
|
||||
|
||||
<md-card>
|
||||
<md-card-subtitle>Without Angular forms</md-card-subtitle>
|
||||
|
||||
<md-select placeholder="Digimon" [(value)]="currentDigimon">
|
||||
<md-option>None</md-option>
|
||||
<md-option *ngFor="let creature of digimon" [value]="creature.value">
|
||||
{{ creature.viewValue }}
|
||||
</md-option>
|
||||
</md-select>
|
||||
|
||||
<p>Value: {{ currentDigimon }}</p>
|
||||
|
||||
<button md-button (click)="currentDigimon='pajiramon-3'">SET VALUE</button>
|
||||
<button md-button (click)="currentDigimon=null">RESET</button>
|
||||
</md-card>
|
||||
|
||||
<md-card>
|
||||
<md-card-subtitle>Option groups</md-card-subtitle>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export class SelectDemo {
|
||||
currentDrink: string;
|
||||
currentPokemon: string[];
|
||||
currentPokemonFromGroup: string;
|
||||
currentDigimon: string;
|
||||
latestChangeEvent: MdSelectChange;
|
||||
floatPlaceholder: string = 'auto';
|
||||
foodControl = new FormControl('pizza-1');
|
||||
@@ -94,6 +95,15 @@ export class SelectDemo {
|
||||
}
|
||||
];
|
||||
|
||||
digimon = [
|
||||
{ value: 'mihiramon-0', viewValue: 'Mihiramon' },
|
||||
{ value: 'sandiramon-1', viewValue: 'Sandiramon' },
|
||||
{ value: 'sinduramon-2', viewValue: 'Sinduramon' },
|
||||
{ value: 'pajiramon-3', viewValue: 'Pajiramon' },
|
||||
{ value: 'vajiramon-4', viewValue: 'Vajiramon' },
|
||||
{ value: 'indramon-5', viewValue: 'Indramon' }
|
||||
];
|
||||
|
||||
toggleDisabled() {
|
||||
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
|
||||
}
|
||||
|
||||
@@ -63,7 +63,10 @@ describe('MdSelect', () => {
|
||||
ResetValuesSelect,
|
||||
FalsyValueSelect,
|
||||
SelectWithGroups,
|
||||
InvalidSelectInForm
|
||||
InvalidSelectInForm,
|
||||
BasicSelectWithoutForms,
|
||||
BasicSelectWithoutFormsPreselected,
|
||||
BasicSelectWithoutFormsMultiple
|
||||
],
|
||||
providers: [
|
||||
{provide: OverlayContainer, useFactory: () => {
|
||||
@@ -706,6 +709,138 @@ describe('MdSelect', () => {
|
||||
|
||||
});
|
||||
|
||||
describe('selection without Angular forms', () => {
|
||||
it('should set the value when options are clicked', () => {
|
||||
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.selectedFood).toBeFalsy();
|
||||
|
||||
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
(overlayContainerElement.querySelector('md-option') as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFood).toBe('steak-0');
|
||||
expect(fixture.componentInstance.select.value).toBe('steak-0');
|
||||
expect(trigger.textContent).toContain('Steak');
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
(overlayContainerElement.querySelectorAll('md-option')[2] as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFood).toBe('sandwich-2');
|
||||
expect(fixture.componentInstance.select.value).toBe('sandwich-2');
|
||||
expect(trigger.textContent).toContain('Sandwich');
|
||||
});
|
||||
|
||||
it('should mark options as selected when the value is set', () => {
|
||||
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.componentInstance.selectedFood = 'sandwich-2';
|
||||
fixture.detectChanges();
|
||||
|
||||
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
expect(trigger.textContent).toContain('Sandwich');
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const option = overlayContainerElement.querySelectorAll('md-option')[2];
|
||||
|
||||
expect(option.classList).toContain('mat-selected');
|
||||
expect(fixture.componentInstance.select.value).toBe('sandwich-2');
|
||||
});
|
||||
|
||||
it('should reset the placeholder when a null value is set', () => {
|
||||
const fixture = TestBed.createComponent(BasicSelectWithoutForms);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.selectedFood).toBeFalsy();
|
||||
|
||||
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
(overlayContainerElement.querySelector('md-option') as HTMLElement).click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFood).toBe('steak-0');
|
||||
expect(fixture.componentInstance.select.value).toBe('steak-0');
|
||||
expect(trigger.textContent).toContain('Steak');
|
||||
|
||||
fixture.componentInstance.selectedFood = null;
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.select.value).toBeNull();
|
||||
expect(trigger.textContent).not.toContain('Steak');
|
||||
});
|
||||
|
||||
it('should reflect the preselected value', async(() => {
|
||||
const fixture = TestBed.createComponent(BasicSelectWithoutFormsPreselected);
|
||||
|
||||
fixture.detectChanges();
|
||||
fixture.whenStable().then(() => {
|
||||
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(trigger.textContent).toContain('Pizza');
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const option = overlayContainerElement.querySelectorAll('md-option')[1];
|
||||
|
||||
expect(option.classList).toContain('mat-selected');
|
||||
expect(fixture.componentInstance.select.value).toBe('pizza-1');
|
||||
});
|
||||
}));
|
||||
|
||||
it('should be able to select multiple values', () => {
|
||||
const fixture = TestBed.createComponent(BasicSelectWithoutFormsMultiple);
|
||||
|
||||
fixture.detectChanges();
|
||||
expect(fixture.componentInstance.selectedFoods).toBeFalsy();
|
||||
|
||||
const trigger = fixture.debugElement.query(By.css('.mat-select-trigger')).nativeElement;
|
||||
|
||||
trigger.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
const options =
|
||||
overlayContainerElement.querySelectorAll('md-option') as NodeListOf<HTMLElement>;
|
||||
|
||||
options[0].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0']);
|
||||
expect(fixture.componentInstance.select.value).toEqual(['steak-0']);
|
||||
expect(trigger.textContent).toContain('Steak');
|
||||
|
||||
options[2].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'sandwich-2']);
|
||||
expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'sandwich-2']);
|
||||
expect(trigger.textContent).toContain('Steak, Sandwich');
|
||||
|
||||
options[1].click();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.componentInstance.selectedFoods).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
|
||||
expect(fixture.componentInstance.select.value).toEqual(['steak-0', 'pizza-1', 'sandwich-2']);
|
||||
expect(trigger.textContent).toContain('Steak, Pizza, Sandwich');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('disabled behavior', () => {
|
||||
|
||||
it('should disable itself when control is disabled programmatically', () => {
|
||||
@@ -2361,7 +2496,6 @@ describe('MdSelect', () => {
|
||||
|
||||
});
|
||||
|
||||
|
||||
describe('reset values', () => {
|
||||
let fixture: ComponentFixture<ResetValuesSelect>;
|
||||
let trigger: HTMLElement;
|
||||
@@ -2892,3 +3026,63 @@ class SelectWithGroups {
|
||||
class InvalidSelectInForm {
|
||||
value: any;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-select placeholder="Food" [(value)]="selectedFood">
|
||||
<md-option *ngFor="let food of foods" [value]="food.value">
|
||||
{{ food.viewValue }}
|
||||
</md-option>
|
||||
</md-select>
|
||||
`
|
||||
})
|
||||
class BasicSelectWithoutForms {
|
||||
selectedFood: string | null;
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
{ value: 'sandwich-2', viewValue: 'Sandwich' },
|
||||
];
|
||||
|
||||
@ViewChild(MdSelect) select: MdSelect;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-select placeholder="Food" [(value)]="selectedFood">
|
||||
<md-option *ngFor="let food of foods" [value]="food.value">
|
||||
{{ food.viewValue }}
|
||||
</md-option>
|
||||
</md-select>
|
||||
`
|
||||
})
|
||||
class BasicSelectWithoutFormsPreselected {
|
||||
selectedFood = 'pizza-1';
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
];
|
||||
|
||||
@ViewChild(MdSelect) select: MdSelect;
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<md-select placeholder="Food" [(value)]="selectedFoods" multiple>
|
||||
<md-option *ngFor="let food of foods" [value]="food.value">
|
||||
{{ food.viewValue }}
|
||||
</md-option>
|
||||
</md-select>
|
||||
`
|
||||
})
|
||||
class BasicSelectWithoutFormsMultiple {
|
||||
selectedFoods: string[];
|
||||
foods: any[] = [
|
||||
{ value: 'steak-0', viewValue: 'Steak' },
|
||||
{ value: 'pizza-1', viewValue: 'Pizza' },
|
||||
{ value: 'sandwich-2', viewValue: 'Sandwich' },
|
||||
];
|
||||
|
||||
@ViewChild(MdSelect) select: MdSelect;
|
||||
}
|
||||
|
||||
@@ -325,6 +325,15 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
}
|
||||
}
|
||||
|
||||
/** Value of the select control. */
|
||||
@Input()
|
||||
get value() { return this._value; }
|
||||
set value(newValue: any) {
|
||||
this.writeValue(newValue);
|
||||
this._value = newValue;
|
||||
}
|
||||
private _value: any;
|
||||
|
||||
/** Aria label of the select. If not specified, the placeholder will be used as label. */
|
||||
@Input('aria-label') ariaLabel: string = '';
|
||||
|
||||
@@ -345,6 +354,13 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
/** Event emitted when the selected value has been changed by the user. */
|
||||
@Output() change: EventEmitter<MdSelectChange> = new EventEmitter<MdSelectChange>();
|
||||
|
||||
/**
|
||||
* Event that emits whenever the raw value of the select changes. This is here primarily
|
||||
* to facilitate the two-way binding for the `value` input.
|
||||
* @docs-private
|
||||
*/
|
||||
@Output() valueChange = new EventEmitter<any>();
|
||||
|
||||
constructor(
|
||||
private _viewportRuler: ViewportRuler,
|
||||
private _changeDetectorRef: ChangeDetectorRef,
|
||||
@@ -377,11 +393,11 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
this._changeSubscription = startWith.call(this.options.changes, null).subscribe(() => {
|
||||
this._resetOptions();
|
||||
|
||||
if (this._control) {
|
||||
// Defer setting the value in order to avoid the "Expression
|
||||
// has changed after it was checked" errors from Angular.
|
||||
Promise.resolve(null).then(() => this._setSelectionByValue(this._control.value));
|
||||
}
|
||||
// Defer setting the value in order to avoid the "Expression
|
||||
// has changed after it was checked" errors from Angular.
|
||||
Promise.resolve().then(() => {
|
||||
this._setSelectionByValue(this._control ? this._control.value : this._value);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -750,8 +766,10 @@ export class MdSelect extends _MdSelectMixinBase implements AfterContentInit, On
|
||||
valueToEmit = this.selected ? this.selected.value : fallbackValue;
|
||||
}
|
||||
|
||||
this._value = valueToEmit;
|
||||
this._onChange(valueToEmit);
|
||||
this.change.emit(new MdSelectChange(this, valueToEmit));
|
||||
this.valueChange.emit(valueToEmit);
|
||||
}
|
||||
|
||||
/** Records option IDs to pass to the aria-owns property. */
|
||||
|
||||
Reference in New Issue
Block a user