Skip to content
This repository has been archived by the owner on Oct 7, 2020. It is now read-only.

Commit

Permalink
feat(select): Implement MdcOption and MdcSelectionModel (#1156)
Browse files Browse the repository at this point in the history
- Add `mdc-option` component (for use in future release)
- Add `MdcSelectionModel` class (for use in future release)

#### mdc-select
- Add `compareWith` input (e.g: `o1: any, o2: any) => boolean`)
- Add `setSelectionByValue(value: any)`
- Add `selectionChange(source: MdcSelect, index: number, value: string)`
- Remove `setValue(value)`
- Remove `selectionChange(index: number, value: string)`
- [x] Update `mdc-select` documentation
- [x] Include unit tests

BREAKING CHANGE:
* Removed `setValue(value: any)`, please use `setSelectionByValue(value: any)` instead.
* Added `source` argument for `selectionChange(source: MdcSelect, index: number, value: string)`

Closes #1155
  • Loading branch information
trimox authored Aug 1, 2018
1 parent 17bbdf3 commit f1f039b
Show file tree
Hide file tree
Showing 10 changed files with 923 additions and 61 deletions.
33 changes: 24 additions & 9 deletions demos/components/select-demo/select-demo.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ <h3>mdc-select</h3>
<td>Text shown if no value has been selected.</td>
</tr>
<tr>
<td>value: string</td>
<td>value: any</td>
<td>Sets the selected item by value.</td>
</tr>
<tr>
Expand Down Expand Up @@ -90,6 +90,10 @@ <h3>mdc-select</h3>
<td>getSelectedIndex(): number</td>
<td>Returns the index of the currently selected option. Returns -1 if no option is currently selected.</td>
</tr>
<tr>
<td>setSelectionByValue(value: any)</td>
<td>Set selection to the passed value.</td>
</tr>
<tr>
<td>isDisabled(): boolean</td>
<td>Returns whether or not the select is disabled.</td>
Expand All @@ -106,13 +110,13 @@ <h3>mdc-select</h3>
<tbody>
<tr>
<td>
change(index: number, value: string)
valueChange(index: number, value: string)
</td>
<td>Event emitted on any value change.</td>
</tr>
<tr>
<td>
selectionChange(index: number, value: string)
selectionChange(source: MdcSelect, index: number, value: string)
</td>
<td>Event emitted if user changed the value.</td>
</tr>
Expand All @@ -125,7 +129,7 @@ <h2>Basic Usage</h2>
</div>

<div class="demo-content">
<h3 class="demo-content__headline">Fruit</h3>
<h3 class="demo-content__headline">Simple</h3>
<mdc-select placeholder="Simple Select">
<option value="" disabled selected></option>
<option value="apple">Apple</option>
Expand All @@ -143,10 +147,20 @@ <h3 class="demo-content__headline">Select without placeholder</h3>
</div>

<div class="demo-content">
<h3 class="demo-content__headline">Select with ngModel </h3>
<h1 mdcHeadline6>Select using compareWith</h1>
<mdc-select [(ngModel)]="currentFoodObject" [compareWith]="compareFn" (selectionChange)="onSelectionChangeTest($event)">
<option *ngFor="let food of foods" [value]="food" [disabled]="food.disabled">
{{ food.viewValue }}
</option>
</mdc-select>
<p> Value: {{ currentFoodObject | json}} </p>
</div>

<div class="demo-content">
<h3 class="demo-content__headline">Select with ngModel</h3>
<div class="demo-content--row">
<button mdc-button (click)="demoSelectModel.reset()">Clear Selection</button>
<button mdc-button (click)="select.setValue('fruit-3')">Select Fruit</button>
<button mdc-button (click)="select.setSelectionByValue('fruit-3')">Select Fruit</button>
<button mdc-button (click)="select.setDisabled(!select.isDisabled())">Disabled: {{select.isDisabled() ? 'On' : 'Off'}}</button>
<button mdc-button (click)="select.box = !select.box">Box: {{select.box ? 'On' : 'Off'}}</button>
<button mdc-button (click)="select.outlined = !select.outlined">Outlined: {{select.outlined ? 'On' : 'Off'}}</button>
Expand All @@ -156,7 +170,7 @@ <h3 class="demo-content__headline">Select with ngModel </h3>
<mdc-select #select placeholder="Favorite food" ngModel #demoSelectModel="ngModel" name="food"
(change)="onChange($event)" (selectionChange)="onSelectionChange($event)">
<option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
{{food.description}}
{{food.viewValue}}
</option>
</mdc-select>
</form>
Expand All @@ -170,15 +184,16 @@ <h3 class="demo-content__headline">Select with ngModel </h3>
<h3 class="demo-content__headline">Select with FormControl</h3>
<div class="demo-content--row">
<button mdc-button (click)="foodControl.reset()">Reset Selection</button>
<button mdc-button (click)="foodControl.setValue('pizza-1')">Select Pizza</button>
<button mdc-button (click)="select2.setSelectionByValue('pizza-1')">Select Pizza</button>
<button mdc-button (click)="foodControl.setValue('pizza-1')">Set Form Value</button>
<button mdc-button (click)="select2.setDisabled(!select2.isDisabled())">Disabled: {{select2.isDisabled() ? 'On' : 'Off'}}</button>
<button mdc-button (click)="select2.box = !select2.box">Box style: {{select2.box ? 'On' : 'Off'}}</button>
<button mdc-button (click)="select2.outlined = !select2.outlined">Outlined: {{select2.outlined ? 'On' : 'Off'}}</button>
<button mdc-button (click)="select2.floatingLabel = !select2.floatingLabel">Floating Label: {{select2.floatingLabel ? 'On' : 'Off'}}</button>
</div>
<mdc-select #select2 placeholder="Favorite food" [formControl]="foodControl">
<option *ngFor="let food of foods" [value]="food.value" [disabled]="food.disabled">
{{food.description}}
{{food.viewValue}}
</option>
</mdc-select>
<p>Value: {{ foodControl.value }}</p>
Expand Down
23 changes: 16 additions & 7 deletions demos/components/select-demo/select-demo.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Component } from '@angular/core';
import { FormControl, ReactiveFormsModule, FormGroup, NgForm, Validators } from '@angular/forms';
import { FormControl, FormGroup, NgForm, Validators } from '@angular/forms';

@Component({
selector: 'select-demo',
Expand All @@ -8,15 +8,20 @@ import { FormControl, ReactiveFormsModule, FormGroup, NgForm, Validators } from
export class SelectDemo {
// foodControl = new FormControl('fruit-3');
foodControl = new FormControl();
currentFoodObject: any;

foods = [
{ value: '', description: '', disabled: false },
{ value: 'steak-0', description: 'Steak' },
{ value: 'pizza-1', description: 'Pizza' },
{ value: 'tacos-2', description: 'Tacos is disabled', disabled: true },
{ value: 'fruit-3', description: 'Fruit' },
{ value: null, viewValue: '', disabled: false },
{ value: 'steak-0', viewValue: 'Steak' },
{ value: 'pizza-1', viewValue: 'Pizza' },
{ value: 'tacos-2', viewValue: 'Tacos is disabled', disabled: true },
{ value: 'fruit-3', viewValue: 'Fruit' },
];

compareFn(f1: { value: any }, f2: { value: any }): boolean {
return f1 && f2 && f1.value === f2.value;
}

constructor() {
// this.foodControl.setValue('fruit-3');
}
Expand All @@ -26,6 +31,10 @@ export class SelectDemo {
}

onSelectionChange(event: { index: any, value: any }) {
console.log(`onSelectionChange: ${event.index}`);
console.log(`onSelectionChange: ${event.index} ${event.value}`);
}
onSelectionChangeTest(event: { index: any, value: any }) {
console.log(this.currentFoodObject)
console.log(`onSelectionChange: ${event.index} ${event.value}`);
}
}
9 changes: 9 additions & 0 deletions packages/common/option-module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NgModule } from '@angular/core';

import { MdcOption } from './option';

@NgModule({
exports: [MdcOption],
declarations: [MdcOption]
})
export class MdcOptionModule { }
190 changes: 190 additions & 0 deletions packages/common/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import {
AfterViewChecked,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostBinding,
Inject,
InjectionToken,
Input,
OnDestroy,
Optional,
Output,
ViewEncapsulation
} from '@angular/core';
import { Subject } from 'rxjs';

import { toBoolean } from './boolean-property';
import { ENTER, SPACE } from './keycodes';

let nextUniqueId = 0;

/**
* Describes a parent component that manages a list of options.
* Contains properties that the options can inherit.
*/
export interface MdcOptionParentComponent {
disableRipple?: boolean;
multiple?: boolean;
}

/**
* Injection token used to provide the parent component to options.
*/
export const MDC_OPTION_PARENT_COMPONENT =
new InjectionToken<MdcOptionParentComponent>('MDC_OPTION_PARENT_COMPONENT');

export class MdcOptionSelectionChange {
constructor(
public source: MdcOption,
public isUserInput = false) { }
}

@Component({
moduleId: module.id,
selector: 'mdc-option',
exportAs: 'mdcOption',
template: `<ng-content></ng-content>`,
host: {
'role': 'option',
'[id]': 'id',
'[attr.tabindex]': '_getTabIndex()',
'[attr.aria-disabled]': 'disabled.toString()',
'(click)': 'selectViaInteraction()',
'(keydown)': 'handleKeydown($event)'
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None
})
export class MdcOption implements AfterViewChecked, OnDestroy {
/** Emits when the state of the option changes and any parents have to be notified. */
readonly _stateChanges = new Subject<void>();

private _selected = false;
private _id: string = `mdc-option-${++nextUniqueId}`;
private _mostRecentViewValue = '';

/** The unique ID of the option. */
get id(): string { return this._id; }

/** Whether or not the option is currently selected. */
get selected(): boolean { return this._selected; }

/** Whether the wrapping component is in multiple selection mode. */
get multiple() { return this._parent && this._parent.multiple; }

@Input() value: any;

@Input()
get disabled() { return this._disabled; }
set disabled(value: boolean) {
this._disabled = toBoolean(value);
}
private _disabled = false;

/** Selects the option. */
select(): void {
if (!this._selected) {
this._selected = true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}

/** Deselects the option. */
deselect(): void {
if (this._selected) {
this._selected = false;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent();
}
}

/** Sets focus onto this option. */
focus(): void {
const element = this._getHostElement();

if (typeof element.focus === 'function') {
element.focus();
}
}

/** Returns the correct tabindex for the option depending on disabled state. */
_getTabIndex(): string {
return this.disabled ? '-1' : '0';
}

_getDisabled(): any {
return this.disabled ? 'disabled' : '';
}

/** Ensures the option is selected when activated from the keyboard. */
handleKeydown(event: KeyboardEvent): void {
if (event.keyCode === ENTER || event.keyCode === SPACE) {
this.selectViaInteraction();

// Prevent the page from scrolling down and form submits.
event.preventDefault();
}
}

/**
* `Selects the option while indicating the selection came from the user. Used to
* determine if the select's view -> model callback should be invoked.`
*/
selectViaInteraction(): void {
if (!this.disabled) {
this._selected = this.multiple ? !this._selected : true;
this._changeDetectorRef.markForCheck();
this._emitSelectionChangeEvent(true);
}
}

@Output() readonly selectionChange = new EventEmitter<MdcOptionSelectionChange>();

/** Gets the label to be used when determining whether the option should be focused. */
getLabel(): string {
return this.viewValue;
}

get viewValue(): string {
return (this._getHostElement().textContent || '').trim();
}

constructor(
public elementRef: ElementRef,
private _changeDetectorRef: ChangeDetectorRef,
@Optional() @Inject(MDC_OPTION_PARENT_COMPONENT) private _parent: MdcOptionParentComponent) { }

ngAfterViewChecked() {
// Since parent components could be using the option's label to display the selected values
// (e.g. `mdc-select`) and they don't have a way of knowing if the option's label has changed
// we have to check for changes in the DOM ourselves and dispatch an event. These checks are
// relatively cheap, however we still limit them only to selected options in order to avoid
// hitting the DOM too often.
if (this._selected) {
const viewValue = this.viewValue;

if (viewValue !== this._mostRecentViewValue) {
this._mostRecentViewValue = viewValue;
this._stateChanges.next();
}
}
}

ngOnDestroy() {
this._stateChanges.complete();
}

/** Emits the selection change event. */
private _emitSelectionChangeEvent(isUserInput = false): void {
this.selectionChange.emit(new MdcOptionSelectionChange(this, isUserInput));
}

/** Retrieves the DOM element of the component host. */
private _getHostElement(): HTMLElement {
return this.elementRef.nativeElement;
}
}
3 changes: 3 additions & 0 deletions packages/common/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,8 @@ export * from './event-registry';
export * from './events';
export * from './keycodes';
export * from './number-property';
export * from './option';
export * from './option-module';
export * from './platform';
export * from './router';
export * from './selection';
Loading

0 comments on commit f1f039b

Please sign in to comment.