Skip to content

Commit 89a49c8

Browse files
committed
* Integrates the SelectionModel into md-select.
* Adds the `multiple` option which allows users to select multiple options from a `md-select`. * Fixes a button that wasn't being cleaned up from dialog tests, causing some select tests to fail. Fixes angular#2412.
1 parent ce0e933 commit 89a49c8

16 files changed

+691
-156
lines changed

src/demo-app/select/select-demo.html

+53-22
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,10 @@
11
<div style="height: 1000px">This div is for testing scrolled selects.</div>
22
<button md-button (click)="showSelect=!showSelect">SHOW SELECT</button>
33
<div class="demo-select">
4-
<div *ngIf="showSelect">
5-
<md-card>
6-
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
7-
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }} </md-option>
8-
</md-select>
9-
<p> Value: {{ foodControl.value }} </p>
10-
<p> Touched: {{ foodControl.touched }} </p>
11-
<p> Dirty: {{ foodControl.dirty }} </p>
12-
<p> Status: {{ foodControl.status }} </p>
13-
<button md-button (click)="foodControl.setValue('pizza-1')">SET VALUE</button>
14-
<button md-button (click)="toggleDisabled()">TOGGLE DISABLED</button>
15-
<button md-button (click)="foodControl.reset()">RESET</button>
16-
</md-card>
17-
</div>
18-
194
<md-card>
20-
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="isRequired" [disabled]="isDisabled"
5+
<md-card-subtitle>ngModel</md-card-subtitle>
6+
7+
<md-select placeholder="Drink" [(ngModel)]="currentDrink" [required]="drinksRequired" [disabled]="drinksDisabled"
218
[floatPlaceholder]="floatPlaceholder" #drinkControl="ngModel">
229
<md-option *ngFor="let drink of drinks" [value]="drink.value" [disabled]="drink.disabled">
2310
{{ drink.viewValue }}
@@ -37,18 +24,62 @@
3724
</p>
3825

3926
<button md-button (click)="currentDrink='water-2'">SET VALUE</button>
40-
<button md-button (click)="isRequired=!isRequired">TOGGLE REQUIRED</button>
41-
<button md-button (click)="isDisabled=!isDisabled">TOGGLE DISABLED</button>
27+
<button md-button (click)="drinksRequired=!drinksRequired">TOGGLE REQUIRED</button>
28+
<button md-button (click)="drinksDisabled=!drinksDisabled">TOGGLE DISABLED</button>
4229
<button md-button (click)="drinkControl.reset()">RESET</button>
4330
</md-card>
4431

32+
<md-card>
33+
<md-card-subtitle>Multiple selection</md-card-subtitle>
34+
35+
<md-card-content>
36+
<md-select multiple placeholder="Pokemon" [(ngModel)]="currentPokemon"
37+
[required]="pokemonRequired" [disabled]="pokemonDisabled" #pokemonControl="ngModel">
38+
<md-option *ngFor="let creature of pokemon" [value]="creature.value">
39+
{{ creature.viewValue }}
40+
</md-option>
41+
</md-select>
42+
<p> Value: {{ currentPokemon }} </p>
43+
<p> Touched: {{ pokemonControl.touched }} </p>
44+
<p> Dirty: {{ pokemonControl.dirty }} </p>
45+
<p> Status: {{ pokemonControl.control?.status }} </p>
46+
<button md-button (click)="currentPokemon=['eevee-4', 'psyduck-6']">SET VALUE</button>
47+
<button md-button (click)="pokemonRequired=!pokemonRequired">TOGGLE REQUIRED</button>
48+
<button md-button (click)="pokemonDisabled=!pokemonDisabled">TOGGLE DISABLED</button>
49+
<button md-button (click)="pokemonControl.reset()">RESET</button>
50+
</md-card-content>
51+
</md-card>
52+
4553
<div *ngIf="showSelect">
4654
<md-card>
47-
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
48-
<md-option *ngFor="let starter of pokemon" [value]="starter.value"> {{ starter.viewValue }} </md-option>
49-
</md-select>
55+
<md-card-subtitle>formControl</md-card-subtitle>
56+
57+
<md-card-content>
58+
<md-select placeholder="Food i would like to eat" [formControl]="foodControl">
59+
<md-option *ngFor="let food of foods" [value]="food.value"> {{ food.viewValue }}</md-option>
60+
</md-select>
61+
<p> Value: {{ foodControl.value }} </p>
62+
<p> Touched: {{ foodControl.touched }} </p>
63+
<p> Dirty: {{ foodControl.dirty }} </p>
64+
<p> Status: {{ foodControl.status }} </p>
65+
<button md-button (click)="foodControl.setValue('pizza-1')">SET VALUE</button>
66+
<button md-button (click)="toggleDisabled()">TOGGLE DISABLED</button>
67+
<button md-button (click)="foodControl.reset()">RESET</button>
68+
</md-card-content>
69+
</md-card>
70+
</div>
71+
72+
<div *ngIf="showSelect">
73+
<md-card>
74+
<md-card-subtitle>Change event</md-card-subtitle>
75+
76+
<md-card-content>
77+
<md-select placeholder="Starter Pokemon" (change)="latestChangeEvent = $event">
78+
<md-option *ngFor="let creature of pokemon" [value]="creature.value">{{ creature.viewValue }}</md-option>
79+
</md-select>
5080

51-
<p> Change event value: {{ latestChangeEvent?.value }} </p>
81+
<p> Change event value: {{ latestChangeEvent?.value }} </p>
82+
</md-card-content>
5283
</md-card>
5384
</div>
5485

src/demo-app/select/select-demo.ts

+10-3
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,13 @@ import {MdSelectChange} from '@angular/material';
99
styleUrls: ['select-demo.css'],
1010
})
1111
export class SelectDemo {
12-
isRequired = false;
13-
isDisabled = false;
12+
drinksRequired = false;
13+
pokemonRequired = false;
14+
drinksDisabled = false;
15+
pokemonDisabled = false;
1416
showSelect = false;
1517
currentDrink: string;
18+
currentPokemon: string[];
1619
latestChangeEvent: MdSelectChange;
1720
floatPlaceholder: string = 'auto';
1821
foodControl = new FormControl('pizza-1');
@@ -38,7 +41,11 @@ export class SelectDemo {
3841
pokemon = [
3942
{value: 'bulbasaur-0', viewValue: 'Bulbasaur'},
4043
{value: 'charizard-1', viewValue: 'Charizard'},
41-
{value: 'squirtle-2', viewValue: 'Squirtle'}
44+
{value: 'squirtle-2', viewValue: 'Squirtle'},
45+
{value: 'pikachu-3', viewValue: 'Pikachu'},
46+
{value: 'eevee-4', viewValue: 'Eevee'},
47+
{value: 'ditto-5', viewValue: 'Ditto'},
48+
{value: 'psyduck-6', viewValue: 'Psyduck'},
4249
];
4350

4451
toggleDisabled() {

src/lib/autocomplete/autocomplete-trigger.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {MdAutocomplete} from './autocomplete';
1515
import {PositionStrategy} from '../core/overlay/position/position-strategy';
1616
import {ConnectedPositionStrategy} from '../core/overlay/position/connected-position-strategy';
1717
import {Observable} from 'rxjs/Observable';
18-
import {MdOptionSelectEvent, MdOption} from '../core/option/option';
18+
import {MdOptionSelectionChange, MdOption} from '../core/option/option';
1919
import {ENTER, UP_ARROW, DOWN_ARROW} from '../core/keyboard/keycodes';
2020
import {Dir} from '../core/rtl/dir';
2121
import {Subscription} from 'rxjs/Subscription';
@@ -146,7 +146,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
146146
* A stream of actions that should close the autocomplete panel, including
147147
* when an option is selected, on blur, and when TAB is pressed.
148148
*/
149-
get panelClosingActions(): Observable<MdOptionSelectEvent> {
149+
get panelClosingActions(): Observable<MdOptionSelectionChange> {
150150
return Observable.merge(
151151
this.optionSelections,
152152
this._blurStream.asObservable(),
@@ -155,8 +155,8 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
155155
}
156156

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

162162
/** The currently active option, coerced to MdOption type. */
@@ -301,7 +301,7 @@ export class MdAutocompleteTrigger implements ControlValueAccessor, OnDestroy {
301301
* control to that value. It will also mark the control as dirty if this interaction
302302
* stemmed from the user.
303303
*/
304-
private _setValueAndClose(event: MdOptionSelectEvent | null): void {
304+
private _setValueAndClose(event: MdOptionSelectionChange | null): void {
305305
if (event) {
306306
this._setTriggerValue(event.source.value);
307307
this._onChange(event.source.value);

src/lib/core/option/_option-theme.scss

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,12 @@
1212
}
1313

1414
&.mat-selected {
15-
background: mat-color($background, hover);
1615
color: mat-color($primary);
16+
17+
// In multiple mode there is a checkbox to show that the option is selected.
18+
&:not(.mat-option-multiple) {
19+
background: mat-color($background, hover);
20+
}
1721
}
1822

1923
&.mat-active {
@@ -26,4 +30,4 @@
2630
}
2731

2832
}
29-
}
33+
}

src/lib/core/option/_option.scss

+10
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,15 @@
3030
opacity: 0.5;
3131
}
3232
}
33+
34+
.mat-option-pseudo-checkbox {
35+
$margin: $mat-menu-side-padding / 2;
36+
margin-right: $margin;
37+
38+
[dir='rtl'] & {
39+
margin-left: $margin;
40+
margin-right: 0;
41+
}
42+
}
3343
}
3444

src/lib/core/option/option.html

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<md-pseudo-checkbox class="mat-option-pseudo-checkbox" *ngIf="multiple"
2+
[state]="selected ? 'checked' : ''" color="primary"></md-pseudo-checkbox>
13
<ng-content></ng-content>
24
<div class="mat-option-ripple" *ngIf="!disabled" md-ripple [mdRippleTrigger]="_getHostElement()">
35
</div>

src/lib/core/option/option.ts

+28-18
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,17 @@ import {CommonModule} from '@angular/common';
1313
import {ENTER, SPACE} from '../keyboard/keycodes';
1414
import {coerceBooleanProperty} from '../coercion/boolean-property';
1515
import {MdRippleModule} from '../ripple/index';
16+
import {MdSelectionModule} from '../selection/index';
1617

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

23-
/** Event object emitted by MdOption when selected. */
24-
export class MdOptionSelectEvent {
25-
constructor(public source: MdOption, public isUserInput = false) {}
24+
/** Event object emitted by MdOption when selected or deselected. */
25+
export class MdOptionSelectionChange {
26+
constructor(public source: MdOption, public isUserInput = false) { }
2627
}
2728

2829

@@ -36,6 +37,7 @@ export class MdOptionSelectEvent {
3637
'role': 'option',
3738
'[attr.tabindex]': '_getTabIndex()',
3839
'[class.mat-selected]': 'selected',
40+
'[class.mat-option-multiple]': 'multiple',
3941
'[class.mat-active]': 'active',
4042
'[id]': 'id',
4143
'[attr.aria-selected]': 'selected.toString()',
@@ -57,9 +59,15 @@ export class MdOption {
5759

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

62+
/** Whether the wrapping component is in multiple selection mode. */
63+
multiple: boolean = false;
64+
6065
/** The unique ID of the option. */
6166
get id() { return this._id; }
6267

68+
/** Whether or not the option is currently selected. */
69+
get selected(): boolean { return this._selected; }
70+
6371
/** The form value of the option. */
6472
@Input() value: any;
6573

@@ -68,16 +76,11 @@ export class MdOption {
6876
get disabled() { return this._disabled; }
6977
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
7078

71-
/** Event emitted when the option is selected. */
72-
@Output() onSelect = new EventEmitter<MdOptionSelectEvent>();
79+
/** Event emitted when the option is selected or deselected. */
80+
@Output() onSelectionChange = new EventEmitter<MdOptionSelectionChange>();
7381

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

76-
/** Whether or not the option is currently selected. */
77-
get selected(): boolean {
78-
return this._selected;
79-
}
80-
8184
/**
8285
* Whether or not the option is currently active and ready to be selected.
8386
* An active option displays styles as if it is focused, but the
@@ -100,12 +103,13 @@ export class MdOption {
100103
/** Selects the option. */
101104
select(): void {
102105
this._selected = true;
103-
this.onSelect.emit(new MdOptionSelectEvent(this, false));
106+
this._emitSelectionChangeEvent();
104107
}
105108

106109
/** Deselects the option. */
107110
deselect(): void {
108111
this._selected = false;
112+
this._emitSelectionChangeEvent();
109113
}
110114

111115
/** Sets focus onto this option. */
@@ -118,7 +122,7 @@ export class MdOption {
118122
* active. This is used by the ActiveDescendantKeyManager so key
119123
* events will display the proper options as active on arrow key events.
120124
*/
121-
setActiveStyles() {
125+
setActiveStyles(): void {
122126
Promise.resolve(null).then(() => this._active = true);
123127
}
124128

@@ -127,7 +131,7 @@ export class MdOption {
127131
* active. This is used by the ActiveDescendantKeyManager so key
128132
* events will display the proper options as active on arrow key events.
129133
*/
130-
setInactiveStyles() {
134+
setInactiveStyles(): void {
131135
Promise.resolve(null).then(() => this._active = false);
132136
}
133137

@@ -142,26 +146,32 @@ export class MdOption {
142146
* Selects the option while indicating the selection came from the user. Used to
143147
* determine if the select's view -> model callback should be invoked.
144148
*/
145-
_selectViaInteraction() {
149+
_selectViaInteraction(): void {
146150
if (!this.disabled) {
147-
this._selected = true;
148-
this.onSelect.emit(new MdOptionSelectEvent(this, true));
151+
this._selected = this.multiple ? !this._selected : true;
152+
this._emitSelectionChangeEvent(true);
149153
}
150154
}
151155

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

161+
/** Fetches the host DOM element. */
157162
_getHostElement(): HTMLElement {
158163
return this._element.nativeElement;
159164
}
160165

166+
/** Emits the selection change event. */
167+
private _emitSelectionChangeEvent(isUserInput = false): void {
168+
this.onSelectionChange.emit(new MdOptionSelectionChange(this, isUserInput));
169+
};
170+
161171
}
162172

163173
@NgModule({
164-
imports: [MdRippleModule, CommonModule],
174+
imports: [MdRippleModule, CommonModule, MdSelectionModule],
165175
exports: [MdOption],
166176
declarations: [MdOption]
167177
})

src/lib/core/selection/pseudo-checkbox/_pseudo-checkbox-theme.scss

-2
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,6 @@
2424
}
2525

2626
.mat-pseudo-checkbox-checked, .mat-pseudo-checkbox-indeterminate {
27-
border: none;
28-
2927
&.mat-primary {
3028
background: mat-color($primary, 500);
3129
}

0 commit comments

Comments
 (0)