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:
Kristiyan Kostadinov
2017-07-26 02:23:58 +03:00
committed by Andrew Seguin
parent ebb5e9e23f
commit 9a90eaf97d
4 changed files with 245 additions and 7 deletions
+16
View File
@@ -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>
+10
View File
@@ -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();
}
+196 -2
View File
@@ -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;
}
+23 -5
View File
@@ -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. */