Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(select): support basic usage without @angular/forms #5871

Merged
merged 2 commits into from
Jul 25, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,22 @@
</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>

Expand Down
10 changes: 10 additions & 0 deletions src/demo-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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();
}
Expand Down
198 changes: 196 additions & 2 deletions src/lib/select/select.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ describe('MdSelect', () => {
ResetValuesSelect,
FalsyValueSelect,
SelectWithGroups,
InvalidSelectInForm
InvalidSelectInForm,
BasicSelectWithoutForms,
BasicSelectWithoutFormsPreselected,
BasicSelectWithoutFormsMultiple
],
providers: [
{provide: OverlayContainer, useFactory: () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -2361,7 +2496,6 @@ describe('MdSelect', () => {

});


describe('reset values', () => {
let fixture: ComponentFixture<ResetValuesSelect>;
let trigger: HTMLElement;
Expand Down Expand Up @@ -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;
}
28 changes: 23 additions & 5 deletions src/lib/select/select.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';

Expand All @@ -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,
Expand Down Expand Up @@ -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);
});
});
}

Expand Down Expand Up @@ -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. */
Expand Down