diff --git a/src/components/button-toggle/button-toggle.spec.ts b/src/components/button-toggle/button-toggle.spec.ts index 12539582b8d2..b027d65f7945 100644 --- a/src/components/button-toggle/button-toggle.spec.ts +++ b/src/components/button-toggle/button-toggle.spec.ts @@ -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; + 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; + 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; let groupDebugElement: DebugElement; @@ -318,6 +453,26 @@ class ButtonTogglesInsideButtonToggleGroup { groupValue: string = null; } +@Component({ + directives: [MD_BUTTON_TOGGLE_DIRECTIVES], + template: ` + + + {{option.label}} + + + ` +}) +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: ` diff --git a/src/components/button-toggle/button-toggle.ts b/src/components/button-toggle/button-toggle.ts index 270991aa3674..543ea3367b43 100644 --- a/src/components/button-toggle/button-toggle.ts +++ b/src/components/button-toggle/button-toggle.ts @@ -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 = new EventEmitter(); @Output() get change(): Observable { @@ -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(); } diff --git a/src/demo-app/button-toggle/button-toggle-demo.html b/src/demo-app/button-toggle/button-toggle-demo.html index 982f87e5a203..9c1a6ae36b4d 100644 --- a/src/demo-app/button-toggle/button-toggle-demo.html +++ b/src/demo-app/button-toggle/button-toggle-demo.html @@ -37,3 +37,13 @@

Multiple Selection

Single Toggle

Yes + +

Dynamic Exclusive Selection

+
+ + + {{pie}} + + +

Your favorite type of pie is: {{favoritePie}}

+
diff --git a/src/demo-app/button-toggle/button-toggle-demo.ts b/src/demo-app/button-toggle/button-toggle-demo.ts index 4cb8335997b9..cd6e4c92db62 100644 --- a/src/demo-app/button-toggle/button-toggle-demo.ts +++ b/src/demo-app/button-toggle/button-toggle-demo.ts @@ -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', + ]; +}