Skip to content

Commit 04bdd16

Browse files
iveysaurjelbourn
authored andcommitted
feat(button-toggle): support ngModel (#694)
* Add ngModel support to button toggles * Remove reference to ngControl * Use onTouched and export the constant
1 parent 84e0079 commit 04bdd16

File tree

4 files changed

+242
-19
lines changed

4 files changed

+242
-19
lines changed

src/components/button-toggle/button-toggle.spec.ts

Lines changed: 156 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,17 @@ import {
88
fakeAsync,
99
tick,
1010
} from '@angular/core/testing';
11+
// TODO(iveysaur): Update to @angular/forms when we have rc.2
12+
import {NgControl} from '@angular/common';
1113
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
1214
import {Component, DebugElement, provide} from '@angular/core';
1315
import {By} from '@angular/platform-browser';
1416
import {
1517
MD_BUTTON_TOGGLE_DIRECTIVES,
1618
MdButtonToggleGroup,
1719
MdButtonToggle,
18-
MdButtonToggleGroupMultiple
20+
MdButtonToggleGroupMultiple,
21+
MdButtonToggleChange,
1922
} from './button-toggle';
2023
import {
2124
MdUniqueSelectionDispatcher
@@ -189,6 +192,138 @@ describe('MdButtonToggle', () => {
189192
});
190193
});
191194

195+
describe('button toggle group with ngModel', () => {
196+
let fixture: ComponentFixture<ButtonToggleGroupWithNgModel>;
197+
let groupDebugElement: DebugElement;
198+
let groupNativeElement: HTMLElement;
199+
let buttonToggleDebugElements: DebugElement[];
200+
let buttonToggleNativeElements: HTMLElement[];
201+
let groupInstance: MdButtonToggleGroup;
202+
let buttonToggleInstances: MdButtonToggle[];
203+
let testComponent: ButtonToggleGroupWithNgModel;
204+
let groupNgControl: NgControl;
205+
206+
beforeEach(async(() => {
207+
builder.createAsync(ButtonToggleGroupWithNgModel).then(f => {
208+
fixture = f;
209+
fixture.detectChanges();
210+
211+
testComponent = fixture.debugElement.componentInstance;
212+
213+
groupDebugElement = fixture.debugElement.query(By.directive(MdButtonToggleGroup));
214+
groupNativeElement = groupDebugElement.nativeElement;
215+
groupInstance = groupDebugElement.injector.get(MdButtonToggleGroup);
216+
groupNgControl = groupDebugElement.injector.get(NgControl);
217+
218+
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
219+
buttonToggleNativeElements =
220+
buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
221+
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
222+
});
223+
}));
224+
225+
it('should set individual radio names based on the group name', () => {
226+
expect(groupInstance.name).toBeTruthy();
227+
for (let buttonToggle of buttonToggleInstances) {
228+
expect(buttonToggle.name).toBe(groupInstance.name);
229+
}
230+
231+
groupInstance.name = 'new name';
232+
for (let buttonToggle of buttonToggleInstances) {
233+
expect(buttonToggle.name).toBe(groupInstance.name);
234+
}
235+
});
236+
237+
it('should check the corresponding button toggle on a group value change', () => {
238+
expect(groupInstance.value).toBeFalsy();
239+
for (let buttonToggle of buttonToggleInstances) {
240+
expect(buttonToggle.checked).toBeFalsy();
241+
}
242+
243+
groupInstance.value = 'red';
244+
for (let buttonToggle of buttonToggleInstances) {
245+
expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value);
246+
}
247+
expect(groupInstance.selected.value).toBe(groupInstance.value);
248+
});
249+
250+
it('should have the correct ngControl state initially and after interaction', fakeAsync(() => {
251+
expect(groupNgControl.valid).toBe(true);
252+
expect(groupNgControl.pristine).toBe(true);
253+
expect(groupNgControl.touched).toBe(false);
254+
255+
buttonToggleInstances[1].checked = true;
256+
fixture.detectChanges();
257+
tick();
258+
259+
expect(groupNgControl.valid).toBe(true);
260+
expect(groupNgControl.pristine).toBe(false);
261+
expect(groupNgControl.touched).toBe(false);
262+
263+
let nativeRadioLabel = buttonToggleDebugElements[2].query(By.css('label')).nativeElement;
264+
nativeRadioLabel.click();
265+
fixture.detectChanges();
266+
tick();
267+
268+
expect(groupNgControl.valid).toBe(true);
269+
expect(groupNgControl.pristine).toBe(false);
270+
expect(groupNgControl.touched).toBe(true);
271+
}));
272+
273+
it('should update the ngModel value when selecting a button toggle', fakeAsync(() => {
274+
buttonToggleInstances[1].checked = true;
275+
fixture.detectChanges();
276+
277+
tick();
278+
279+
expect(testComponent.modelValue).toBe('green');
280+
}));
281+
});
282+
283+
describe('button toggle group with ngModel and change event', () => {
284+
let fixture: ComponentFixture<ButtonToggleGroupWithNgModel>;
285+
let groupDebugElement: DebugElement;
286+
let groupNativeElement: HTMLElement;
287+
let buttonToggleDebugElements: DebugElement[];
288+
let buttonToggleNativeElements: HTMLElement[];
289+
let groupInstance: MdButtonToggleGroup;
290+
let buttonToggleInstances: MdButtonToggle[];
291+
let testComponent: ButtonToggleGroupWithNgModel;
292+
let groupNgControl: NgControl;
293+
294+
beforeEach(async(() => {
295+
builder.createAsync(ButtonToggleGroupWithNgModel).then(f => {
296+
fixture = f;
297+
298+
testComponent = fixture.debugElement.componentInstance;
299+
300+
groupDebugElement = fixture.debugElement.query(By.directive(MdButtonToggleGroup));
301+
groupNativeElement = groupDebugElement.nativeElement;
302+
groupInstance = groupDebugElement.injector.get(MdButtonToggleGroup);
303+
groupNgControl = groupDebugElement.injector.get(NgControl);
304+
305+
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
306+
buttonToggleNativeElements =
307+
buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
308+
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
309+
310+
fixture.detectChanges();
311+
});
312+
}));
313+
314+
it('should update the model before firing change event', fakeAsync(() => {
315+
expect(testComponent.modelValue).toBeUndefined();
316+
expect(testComponent.lastEvent).toBeUndefined();
317+
318+
groupInstance.value = 'red';
319+
fixture.detectChanges();
320+
321+
tick();
322+
expect(testComponent.modelValue).toBe('red');
323+
expect(testComponent.lastEvent.value).toBe('red');
324+
}));
325+
});
326+
192327
describe('inside of a multiple selection group', () => {
193328
let fixture: ComponentFixture<ButtonTogglesInsideButtonToggleGroupMultiple>;
194329
let groupDebugElement: DebugElement;
@@ -318,6 +453,26 @@ class ButtonTogglesInsideButtonToggleGroup {
318453
groupValue: string = null;
319454
}
320455

456+
@Component({
457+
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
458+
template: `
459+
<md-button-toggle-group [(ngModel)]="modelValue" (change)="lastEvent = $event">
460+
<md-button-toggle *ngFor="let option of options" [value]="option.value">
461+
{{option.label}}
462+
</md-button-toggle>
463+
</md-button-toggle-group>
464+
`
465+
})
466+
class ButtonToggleGroupWithNgModel {
467+
modelValue: string;
468+
options = [
469+
{label: 'Red', value: 'red'},
470+
{label: 'Green', value: 'green'},
471+
{label: 'Blue', value: 'blue'},
472+
];
473+
lastEvent: MdButtonToggleChange;
474+
}
475+
321476
@Component({
322477
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
323478
template: `

src/components/button-toggle/button-toggle.ts

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,43 @@
11
import {
2-
Component,
3-
ContentChildren,
4-
Directive,
5-
EventEmitter,
6-
HostBinding,
7-
Input,
8-
OnInit,
9-
Optional,
10-
Output,
11-
QueryList,
12-
ViewEncapsulation,
13-
forwardRef
2+
Component,
3+
ContentChildren,
4+
Directive,
5+
EventEmitter,
6+
HostBinding,
7+
Input,
8+
OnInit,
9+
Optional,
10+
Output,
11+
Provider,
12+
QueryList,
13+
ViewEncapsulation,
14+
forwardRef
1415
} from '@angular/core';
16+
// TODO(iveysaur): Update to @angular/forms when we have rc.2
17+
import {
18+
NG_VALUE_ACCESSOR,
19+
ControlValueAccessor,
20+
} from '@angular/common';
1521
import {Observable} from 'rxjs/Observable';
1622
import {
17-
MdUniqueSelectionDispatcher
23+
MdUniqueSelectionDispatcher
1824
} from '@angular2-material/core/coordination/unique-selection-dispatcher';
1925
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
2026

2127
export type ToggleType = 'checkbox' | 'radio';
2228

2329

30+
31+
/**
32+
* Provider Expression that allows md-button-toggle-group to register as a ControlValueAccessor.
33+
* This allows it to support [(ngModel)].
34+
*/
35+
export const MD_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR = new Provider(
36+
NG_VALUE_ACCESSOR, {
37+
useExisting: forwardRef(() => MdButtonToggleGroup),
38+
multi: true
39+
});
40+
2441
var _uniqueIdCounter = 0;
2542

2643
/** A simple change event emitted by either MdButtonToggle or MdButtonToggleGroup. */
@@ -32,11 +49,12 @@ export class MdButtonToggleChange {
3249
/** Exclusive selection button toggle group that behaves like a radio-button group. */
3350
@Directive({
3451
selector: 'md-button-toggle-group:not([multiple])',
52+
providers: [MD_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR],
3553
host: {
3654
'role': 'radiogroup',
3755
},
3856
})
39-
export class MdButtonToggleGroup {
57+
export class MdButtonToggleGroup implements ControlValueAccessor {
4058
/** The value for the button toggle group. Should match currently selected button toggle. */
4159
private _value: any = null;
4260

@@ -49,6 +67,12 @@ export class MdButtonToggleGroup {
4967
/** The currently selected button toggle, should match the value. */
5068
private _selected: MdButtonToggle = null;
5169

70+
/** The method to be called in order to update ngModel. */
71+
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
72+
73+
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
74+
onTouched: () => any = () => {};
75+
5276
/** Event emitted when the group's value changes. */
5377
private _change: EventEmitter<MdButtonToggleChange> = new EventEmitter<MdButtonToggleChange>();
5478
@Output() get change(): Observable<MdButtonToggleChange> {
@@ -66,7 +90,6 @@ export class MdButtonToggleGroup {
6690

6791
set name(value: string) {
6892
this._name = value;
69-
7093
this._updateButtonToggleNames();
7194
}
7295

@@ -126,7 +149,9 @@ export class MdButtonToggleGroup {
126149
this.selected = matchingButtonToggle;
127150
} else if (this.value == null) {
128151
this.selected = null;
129-
this._buttonToggles.forEach(buttonToggle => {buttonToggle.checked = false; });
152+
this._buttonToggles.forEach(buttonToggle => {
153+
buttonToggle.checked = false;
154+
});
130155
}
131156
}
132157
}
@@ -136,8 +161,33 @@ export class MdButtonToggleGroup {
136161
let event = new MdButtonToggleChange();
137162
event.source = this._selected;
138163
event.value = this._value;
164+
this._controlValueAccessorChangeFn(event.value);
139165
this._change.emit(event);
140166
}
167+
168+
/**
169+
* Implemented as part of ControlValueAccessor.
170+
* TODO: internal
171+
*/
172+
writeValue(value: any) {
173+
this.value = value;
174+
}
175+
176+
/**
177+
* Implemented as part of ControlValueAccessor.
178+
* TODO: internal
179+
*/
180+
registerOnChange(fn: (value: any) => void) {
181+
this._controlValueAccessorChangeFn = fn;
182+
}
183+
184+
/**
185+
* Implemented as part of ControlValueAccessor.
186+
* TODO: internal
187+
*/
188+
registerOnTouched(fn: any) {
189+
this.onTouched = fn;
190+
}
141191
}
142192

143193
/** Multiple selection button-toggle group. */
@@ -239,7 +289,6 @@ export class MdButtonToggle implements OnInit {
239289
if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
240290
this._checked = true;
241291
}
242-
243292
}
244293

245294
get inputId(): string {
@@ -322,6 +371,7 @@ export class MdButtonToggle implements OnInit {
322371
// button toggle as checked.
323372
this.checked = true;
324373
this.buttonToggleGroup.selected = this;
374+
this.buttonToggleGroup.onTouched();
325375
} else {
326376
this._toggle();
327377
}

src/demo-app/button-toggle/button-toggle-demo.html

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,13 @@ <h1>Multiple Selection</h1>
3737

3838
<h1>Single Toggle</h1>
3939
<md-button-toggle>Yes</md-button-toggle>
40+
41+
<h1>Dynamic Exclusive Selection</h1>
42+
<section class="demo-section">
43+
<md-button-toggle-group name="pies" [(ngModel)]="favoritePie">
44+
<md-button-toggle *ngFor="let pie of pieOptions" [value]="pie">
45+
{{pie}}
46+
</md-button-toggle>
47+
</md-button-toggle-group>
48+
<p>Your favorite type of pie is: {{favoritePie}}</p>
49+
</section>

src/demo-app/button-toggle/button-toggle-demo.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,12 @@ import {MdIcon} from '@angular2-material/icon/icon';
1212
providers: [MdUniqueSelectionDispatcher],
1313
directives: [MD_BUTTON_TOGGLE_DIRECTIVES, MdIcon]
1414
})
15-
export class ButtonToggleDemo { }
15+
export class ButtonToggleDemo {
16+
favoritePie = 'Apple';
17+
pieOptions = [
18+
'Apple',
19+
'Cherry',
20+
'Pecan',
21+
'Lemon',
22+
];
23+
}

0 commit comments

Comments
 (0)