Skip to content

feat(select): add multiple selection mode #2722

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

Merged
merged 5 commits into from
Mar 9, 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
75 changes: 53 additions & 22 deletions src/demo-app/select/select-demo.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,10 @@
<div style="height: 1000px">This div is for testing scrolled selects.</div>
<button md-button (click)="showSelect=!showSelect">SHOW SELECT</button>
<div class="demo-select">
<div *ngIf="showSelect">
<md-card>
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }} </md-option>
</md-select>
<p> Value: {{ foodControl.value }} </p>
<p> Touched: {{ foodControl.touched }} </p>
<p> Dirty: {{ foodControl.dirty }} </p>
<p> Status: {{ foodControl.status }} </p>
<button md-button (click)="foodControl.setValue('pizza-1')">SET VALUE</button>
<button md-button (click)="toggleDisabled()">TOGGLE DISABLED</button>
<button md-button (click)="foodControl.reset()">RESET</button>
</md-card>
</div>

<md-card>
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="isRequired" [disabled]="isDisabled"
<md-card-subtitle>ngModel</md-card-subtitle>

<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
{{ drink.viewValue }}
Expand All @@ -37,18 +24,62 @@
</p>

<button md-button (click)="currentDrink='water-2'">SET VALUE</button>
<button md-button (click)="isRequired=!isRequired">TOGGLE REQUIRED</button>
<button md-button (click)="isDisabled=!isDisabled">TOGGLE DISABLED</button>
<button md-button (click)="drinksRequired=!drinksRequired">TOGGLE REQUIRED</button>
<button md-button (click)="drinksDisabled=!drinksDisabled">TOGGLE DISABLED</button>
<button md-button (click)="drinkControl.reset()">RESET</button>
</md-card>

<md-card>
<md-card-subtitle>Multiple selection</md-card-subtitle>

<md-card-content>
<md-select multiple placeholder="Pokemon" [(ngModel)]="currentPokemon"
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">
{{ creature.viewValue }}
</md-option>
</md-select>
<p> Value: {{ currentPokemon }} </p>
<p> Touched: {{ pokemonControl.touched }} </p>
<p> Dirty: {{ pokemonControl.dirty }} </p>
<p> Status: {{ pokemonControl.control?.status }} </p>
<button md-button (click)="setPokemonValue()">SET VALUE</button>
<button md-button (click)="pokemonRequired=!pokemonRequired">TOGGLE REQUIRED</button>
<button md-button (click)="pokemonDisabled=!pokemonDisabled">TOGGLE DISABLED</button>
<button md-button (click)="pokemonControl.reset()">RESET</button>
</md-card-content>
</md-card>

<div *ngIf="showSelect">
<md-card>
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
<md-option *ngFor="let starter of pokemon" [value]="starter.value"> {{ starter.viewValue }} </md-option>
</md-select>
<md-card-subtitle>formControl</md-card-subtitle>

<md-card-content>
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }}</md-option>
</md-select>
<p> Value: {{ foodControl.value }} </p>
<p> Touched: {{ foodControl.touched }} </p>
<p> Dirty: {{ foodControl.dirty }} </p>
<p> Status: {{ foodControl.status }} </p>
<button md-button (click)="foodControl.setValue('pizza-1')">SET VALUE</button>
<button md-button (click)="toggleDisabled()">TOGGLE DISABLED</button>
<button md-button (click)="foodControl.reset()">RESET</button>
</md-card-content>
</md-card>
</div>

<div *ngIf="showSelect">
<md-card>
<md-card-subtitle>Change event</md-card-subtitle>

<md-card-content>
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
<md-option *ngFor="let creature of pokemon" [value]="creature.value">{{ creature.viewValue }}</md-option>
</md-select>

<p> Change event value: {{ latestChangeEvent?.value }} </p>
<p> Change event value: {{ latestChangeEvent?.value }} </p>
</md-card-content>
</md-card>
</div>

Expand Down
17 changes: 14 additions & 3 deletions src/demo-app/select/select-demo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ import {MdSelectChange} from '@angular/material';
styleUrls: ['select-demo.css'],
})
export class SelectDemo {
isRequired = false;
isDisabled = false;
drinksRequired = false;
pokemonRequired = false;
drinksDisabled = false;
pokemonDisabled = false;
showSelect = false;
currentDrink: string;
currentPokemon: string[];
latestChangeEvent: MdSelectChange;
floatPlaceholder: string = 'auto';
foodControl = new FormControl('pizza-1');
Expand All @@ -38,10 +41,18 @@ export class SelectDemo {
pokemon = [
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
{value: 'charizard-1', viewValue: 'Charizard'},
{value: 'squirtle-2', viewValue: 'Squirtle'}
{value: 'squirtle-2', viewValue: 'Squirtle'},
{value: 'pikachu-3', viewValue: 'Pikachu'},
{value: 'eevee-4', viewValue: 'Eevee'},
{value: 'ditto-5', viewValue: 'Ditto'},
{value: 'psyduck-6', viewValue: 'Psyduck'},
];

toggleDisabled() {
this.foodControl.enabled ? this.foodControl.disable() : this.foodControl.enable();
}

setPokemonValue() {
this.currentPokemon = ['eevee-4', 'psyduck-6'];
}
}
10 changes: 5 additions & 5 deletions src/lib/autocomplete/autocomplete-trigger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {MdAutocomplete} from './autocomplete';
import {PositionStrategy} from '../core/overlay/position/position-strategy';
import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy';
import {Observable} from 'rxjs/Observable';
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
import {Dir} from '../core/rtl/dir';
import {Subscription} from 'rxjs/Subscription';
Expand Down Expand Up @@ -146,7 +146,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* A stream of actions that should close the autocomplete panel, including
* when an option is selected, on blur, and when TAB is pressed.
*/
get panelClosingActions(): Observable<MdOptionSelectEvent> {
get panelClosingActions(): Observable<MdOptionSelectionChange> {
return Observable.merge(
this.optionSelections,
this._blurStream.asObservable(),
Expand All @@ -155,8 +155,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
}

/** Stream of autocomplete option selections. */
get optionSelections(): Observable<MdOptionSelectEvent> {
return Observable.merge(...this.autocomplete.options.map(option => option.onSelect));
get optionSelections(): Observable<MdOptionSelectionChange> {
return Observable.merge(...this.autocomplete.options.map(option => option.onSelectionChange));
}

/** The currently active option, coerced to MdOption type. */
Expand Down Expand Up @@ -301,7 +301,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
* control to that value. It will also mark the control as dirty if this interaction
* stemmed from the user.
*/
private _setValueAndClose(event: MdOptionSelectEvent | null): void {
private _setValueAndClose(event: MdOptionSelectionChange | null): void {
if (event) {
this._setTriggerValue(event.source.value);
this._onChange(event.source.value);
Expand Down
8 changes: 6 additions & 2 deletions src/lib/core/option/_option-theme.scss
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@
}

&.mat-selected {
background: mat-color($background, hover);
color: mat-color($primary);

// In multiple mode there is a checkbox to show that the option is selected.
&:not(.mat-option-multiple) {
background: mat-color($background, hover);
}
}

&.mat-active {
Expand All @@ -26,4 +30,4 @@
}

}
}
}
10 changes: 10 additions & 0 deletions src/lib/core/option/_option.scss
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,15 @@
opacity: 0.5;
}
}

.mat-option-pseudo-checkbox {
$margin: $mat-menu-side-padding / 2;
margin-right: $margin;

[dir='rtl'] & {
margin-left: $margin;
margin-right: 0;
}
}
}

7 changes: 7 additions & 0 deletions src/lib/core/option/option.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
<span [ngSwitch]="_isCompatibilityMode" *ngIf="multiple">
<mat-pseudo-checkbox class="mat-option-pseudo-checkbox" *ngSwitchCase="true"
[state]="selected ? 'checked' : ''" color="primary"></mat-pseudo-checkbox>
<md-pseudo-checkbox class="mat-option-pseudo-checkbox" *ngSwitchDefault
[state]="selected ? 'checked' : ''" color="primary"></md-pseudo-checkbox>
</span>

<ng-content></ng-content>
<div class="mat-option-ripple" *ngIf="!disabled" md-ripple [mdRippleTrigger]="_getHostElement()">
</div>
56 changes: 36 additions & 20 deletions src/lib/core/option/option.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,26 @@ import {
NgModule,
ModuleWithProviders,
Renderer,
ViewEncapsulation
ViewEncapsulation,
Inject,
Optional,
} from '@angular/core';
import {CommonModule} from '@angular/common';
import {ENTER, SPACE} from '../keyboard/keycodes';
import {coerceBooleanProperty} from '../coercion/boolean-property';
import {MdRippleModule} from '../ripple/index';
import {MdSelectionModule} from '../selection/index';
import {MATERIAL_COMPATIBILITY_MODE} from '../../core/compatibility/compatibility';

/**
* Option IDs need to be unique across components, so this counter exists outside of
* the component definition.
*/
let _uniqueIdCounter = 0;

/** Event object emitted by MdOption when selected. */
export class MdOptionSelectEvent {
constructor(public source: MdOption, public isUserInput = false) {}
/** Event object emitted by MdOption when selected or deselected. */
export class MdOptionSelectionChange {
constructor(public source: MdOption, public isUserInput = false) { }
}


Expand All @@ -36,6 +40,7 @@ export class MdOptionSelectEvent {
'role': 'option',
'[attr.tabindex]': '_getTabIndex()',
'[class.mat-selected]': 'selected',
'[class.mat-option-multiple]': 'multiple',
'[class.mat-active]': 'active',
'[id]': 'id',
'[attr.aria-selected]': 'selected.toString()',
Expand All @@ -57,9 +62,15 @@ export class MdOption {

private _id: string = `md-option-${_uniqueIdCounter++}`;

/** Whether the wrapping component is in multiple selection mode. */
multiple: boolean = false;

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

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

/** The form value of the option. */
@Input() value: any;

Expand All @@ -68,15 +79,13 @@ export class MdOption {
get disabled() { return this._disabled; }
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }

/** Event emitted when the option is selected. */
@Output() onSelect = new EventEmitter<MdOptionSelectEvent>();
/** Event emitted when the option is selected or deselected. */
@Output() onSelectionChange = new EventEmitter<MdOptionSelectionChange>();

constructor(private _element: ElementRef, private _renderer: Renderer) {}

/** Whether or not the option is currently selected. */
get selected(): boolean {
return this._selected;
}
constructor(
private _element: ElementRef,
private _renderer: Renderer,
@Optional() @Inject(MATERIAL_COMPATIBILITY_MODE) public _isCompatibilityMode: boolean) {}

/**
* Whether or not the option is currently active and ready to be selected.
Expand All @@ -100,12 +109,13 @@ export class MdOption {
/** Selects the option. */
select(): void {
this._selected = true;
this.onSelect.emit(new MdOptionSelectEvent(this, false));
this._emitSelectionChangeEvent();
}

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

/** Sets focus onto this option. */
Expand All @@ -118,7 +128,7 @@ export class MdOption {
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setActiveStyles() {
setActiveStyles(): void {
Promise.resolve(null).then(() => this._active = true);
}

Expand All @@ -127,7 +137,7 @@ export class MdOption {
* active. This is used by the ActiveDescendantKeyManager so key
* events will display the proper options as active on arrow key events.
*/
setInactiveStyles() {
setInactiveStyles(): void {
Promise.resolve(null).then(() => this._active = false);
}

Expand All @@ -142,26 +152,32 @@ export class MdOption {
* 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() {
_selectViaInteraction(): void {
if (!this.disabled) {
this._selected = true;
this.onSelect.emit(new MdOptionSelectEvent(this, true));
this._selected = this.multiple ? !this._selected : true;
this._emitSelectionChangeEvent(true);
}
}

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

/** Fetches the host DOM element. */
_getHostElement(): HTMLElement {
return this._element.nativeElement;
}

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

}

@NgModule({
imports: [MdRippleModule, CommonModule],
imports: [MdRippleModule, CommonModule, MdSelectionModule],
exports: [MdOption],
declarations: [MdOption]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@
}

.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate {
border: none;

&.mat-primary {
background: mat-color($primary, 500);
}
Expand Down
Loading