mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
Jeremy Elbourn
parent
84e0079df4
commit
04bdd160e7
@@ -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: `
|
||||
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user