feat(button-toggle): support ngModel (#694)

* Add ngModel support to button toggles

* Remove reference to ngControl

* Use onTouched and export the constant
This commit is contained in:
Ivey Padgett
2016-06-24 13:33:38 -07:00
committed by Jeremy Elbourn
parent 84e0079df4
commit 04bdd160e7
4 changed files with 242 additions and 19 deletions
@@ -8,6 +8,8 @@ import {
fakeAsync,
tick,
} from '@angular/core/testing';
// TODO(iveysaur): Update to @angular/forms when we have rc.2
import {NgControl} from '@angular/common';
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
import {Component, DebugElement, provide} from '@angular/core';
import {By} from '@angular/platform-browser';
@@ -15,7 +17,8 @@ import {
MD_BUTTON_TOGGLE_DIRECTIVES,
MdButtonToggleGroup,
MdButtonToggle,
MdButtonToggleGroupMultiple
MdButtonToggleGroupMultiple,
MdButtonToggleChange,
} from './button-toggle';
import {
MdUniqueSelectionDispatcher
@@ -189,6 +192,138 @@ describe('MdButtonToggle', () => {
});
});
describe('button toggle group with ngModel', () => {
let fixture: ComponentFixture<ButtonToggleGroupWithNgModel>;
let groupDebugElement: DebugElement;
let groupNativeElement: HTMLElement;
let buttonToggleDebugElements: DebugElement[];
let buttonToggleNativeElements: HTMLElement[];
let groupInstance: MdButtonToggleGroup;
let buttonToggleInstances: MdButtonToggle[];
let testComponent: ButtonToggleGroupWithNgModel;
let groupNgControl: NgControl;
beforeEach(async(() => {
builder.createAsync(ButtonToggleGroupWithNgModel).then(f => {
fixture = f;
fixture.detectChanges();
testComponent = fixture.debugElement.componentInstance;
groupDebugElement = fixture.debugElement.query(By.directive(MdButtonToggleGroup));
groupNativeElement = groupDebugElement.nativeElement;
groupInstance = groupDebugElement.injector.get(MdButtonToggleGroup);
groupNgControl = groupDebugElement.injector.get(NgControl);
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
buttonToggleNativeElements =
buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
});
}));
it('should set individual radio names based on the group name', () => {
expect(groupInstance.name).toBeTruthy();
for (let buttonToggle of buttonToggleInstances) {
expect(buttonToggle.name).toBe(groupInstance.name);
}
groupInstance.name = 'new name';
for (let buttonToggle of buttonToggleInstances) {
expect(buttonToggle.name).toBe(groupInstance.name);
}
});
it('should check the corresponding button toggle on a group value change', () => {
expect(groupInstance.value).toBeFalsy();
for (let buttonToggle of buttonToggleInstances) {
expect(buttonToggle.checked).toBeFalsy();
}
groupInstance.value = 'red';
for (let buttonToggle of buttonToggleInstances) {
expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value);
}
expect(groupInstance.selected.value).toBe(groupInstance.value);
});
it('should have the correct ngControl state initially and after interaction', fakeAsync(() => {
expect(groupNgControl.valid).toBe(true);
expect(groupNgControl.pristine).toBe(true);
expect(groupNgControl.touched).toBe(false);
buttonToggleInstances[1].checked = true;
fixture.detectChanges();
tick();
expect(groupNgControl.valid).toBe(true);
expect(groupNgControl.pristine).toBe(false);
expect(groupNgControl.touched).toBe(false);
let nativeRadioLabel = buttonToggleDebugElements[2].query(By.css('label')).nativeElement;
nativeRadioLabel.click();
fixture.detectChanges();
tick();
expect(groupNgControl.valid).toBe(true);
expect(groupNgControl.pristine).toBe(false);
expect(groupNgControl.touched).toBe(true);
}));
it('should update the ngModel value when selecting a button toggle', fakeAsync(() => {
buttonToggleInstances[1].checked = true;
fixture.detectChanges();
tick();
expect(testComponent.modelValue).toBe('green');
}));
});
describe('button toggle group with ngModel and change event', () => {
let fixture: ComponentFixture<ButtonToggleGroupWithNgModel>;
let groupDebugElement: DebugElement;
let groupNativeElement: HTMLElement;
let buttonToggleDebugElements: DebugElement[];
let buttonToggleNativeElements: HTMLElement[];
let groupInstance: MdButtonToggleGroup;
let buttonToggleInstances: MdButtonToggle[];
let testComponent: ButtonToggleGroupWithNgModel;
let groupNgControl: NgControl;
beforeEach(async(() => {
builder.createAsync(ButtonToggleGroupWithNgModel).then(f => {
fixture = f;
testComponent = fixture.debugElement.componentInstance;
groupDebugElement = fixture.debugElement.query(By.directive(MdButtonToggleGroup));
groupNativeElement = groupDebugElement.nativeElement;
groupInstance = groupDebugElement.injector.get(MdButtonToggleGroup);
groupNgControl = groupDebugElement.injector.get(NgControl);
buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
buttonToggleNativeElements =
buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
fixture.detectChanges();
});
}));
it('should update the model before firing change event', fakeAsync(() => {
expect(testComponent.modelValue).toBeUndefined();
expect(testComponent.lastEvent).toBeUndefined();
groupInstance.value = 'red';
fixture.detectChanges();
tick();
expect(testComponent.modelValue).toBe('red');
expect(testComponent.lastEvent.value).toBe('red');
}));
});
describe('inside of a multiple selection group', () => {
let fixture: ComponentFixture<ButtonTogglesInsideButtonToggleGroupMultiple>;
let groupDebugElement: DebugElement;
@@ -318,6 +453,26 @@ class ButtonTogglesInsideButtonToggleGroup {
groupValue: string = null;
}
@Component({
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
template: `
<md-button-toggle-group [(ngModel)]="modelValue" (change)="lastEvent = $event">
<md-button-toggle *ngFor="let option of options" [value]="option.value">
{{option.label}}
</md-button-toggle>
</md-button-toggle-group>
`
})
class ButtonToggleGroupWithNgModel {
modelValue: string;
options = [
{label: 'Red', value: 'red'},
{label: 'Green', value: 'green'},
{label: 'Blue', value: 'blue'},
];
lastEvent: MdButtonToggleChange;
}
@Component({
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
template: `
+67 -17
View File
@@ -1,26 +1,43 @@
import {
Component,
ContentChildren,
Directive,
EventEmitter,
HostBinding,
Input,
OnInit,
Optional,
Output,
QueryList,
ViewEncapsulation,
forwardRef
Component,
ContentChildren,
Directive,
EventEmitter,
HostBinding,
Input,
OnInit,
Optional,
Output,
Provider,
QueryList,
ViewEncapsulation,
forwardRef
} from '@angular/core';
// TODO(iveysaur): Update to @angular/forms when we have rc.2
import {
NG_VALUE_ACCESSOR,
ControlValueAccessor,
} from '@angular/common';
import {Observable} from 'rxjs/Observable';
import {
MdUniqueSelectionDispatcher
MdUniqueSelectionDispatcher
} from '@angular2-material/core/coordination/unique-selection-dispatcher';
import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
export type ToggleType = 'checkbox' | 'radio';
/**
* Provider Expression that allows md-button-toggle-group to register as a ControlValueAccessor.
* This allows it to support [(ngModel)].
*/
export const MD_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {
useExisting: forwardRef(() => MdButtonToggleGroup),
multi: true
});
var _uniqueIdCounter = 0;
/** A simple change event emitted by either MdButtonToggle or MdButtonToggleGroup. */
@@ -32,11 +49,12 @@ export class MdButtonToggleChange {
/** Exclusive selection button toggle group that behaves like a radio-button group. */
@Directive({
selector: 'md-button-toggle-group:not([multiple])',
providers: [MD_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR],
host: {
'role': 'radiogroup',
},
})
export class MdButtonToggleGroup {
export class MdButtonToggleGroup implements ControlValueAccessor {
/** The value for the button toggle group. Should match currently selected button toggle. */
private _value: any = null;
@@ -49,6 +67,12 @@ export class MdButtonToggleGroup {
/** The currently selected button toggle, should match the value. */
private _selected: MdButtonToggle = null;
/** The method to be called in order to update ngModel. */
private _controlValueAccessorChangeFn: (value: any) => void = (value) => {};
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
onTouched: () => any = () => {};
/** Event emitted when the group's value changes. */
private _change: EventEmitter<MdButtonToggleChange> = new EventEmitter<MdButtonToggleChange>();
@Output() get change(): Observable<MdButtonToggleChange> {
@@ -66,7 +90,6 @@ export class MdButtonToggleGroup {
set name(value: string) {
this._name = value;
this._updateButtonToggleNames();
}
@@ -126,7 +149,9 @@ export class MdButtonToggleGroup {
this.selected = matchingButtonToggle;
} else if (this.value == null) {
this.selected = null;
this._buttonToggles.forEach(buttonToggle => {buttonToggle.checked = false; });
this._buttonToggles.forEach(buttonToggle => {
buttonToggle.checked = false;
});
}
}
}
@@ -136,8 +161,33 @@ export class MdButtonToggleGroup {
let event = new MdButtonToggleChange();
event.source = this._selected;
event.value = this._value;
this._controlValueAccessorChangeFn(event.value);
this._change.emit(event);
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
writeValue(value: any) {
this.value = value;
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
registerOnChange(fn: (value: any) => void) {
this._controlValueAccessorChangeFn = fn;
}
/**
* Implemented as part of ControlValueAccessor.
* TODO: internal
*/
registerOnTouched(fn: any) {
this.onTouched = fn;
}
}
/** Multiple selection button-toggle group. */
@@ -239,7 +289,6 @@ export class MdButtonToggle implements OnInit {
if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
this._checked = true;
}
}
get inputId(): string {
@@ -322,6 +371,7 @@ export class MdButtonToggle implements OnInit {
// button toggle as checked.
this.checked = true;
this.buttonToggleGroup.selected = this;
this.buttonToggleGroup.onTouched();
} else {
this._toggle();
}
@@ -37,3 +37,13 @@
<h1>Single Toggle</h1>
<md-button-toggle>Yes</md-button-toggle>
<h1>Dynamic Exclusive Selection</h1>
<section class="demo-section">
<md-button-toggle-group name="pies" [(ngModel)]="favoritePie">
<md-button-toggle *ngFor="let pie of pieOptions" [value]="pie">
{{pie}}
</md-button-toggle>
</md-button-toggle-group>
<p>Your favorite type of pie is: {{favoritePie}}</p>
</section>
@@ -12,4 +12,12 @@ import {MdIcon} from '@angular2-material/icon/icon';
providers: [MdUniqueSelectionDispatcher],
directives: [MD_BUTTON_TOGGLE_DIRECTIVES, MdIcon]
})
export class ButtonToggleDemo { }
export class ButtonToggleDemo {
favoritePie = 'Apple';
pieOptions = [
'Apple',
'Cherry',
'Pecan',
'Lemon',
];
}