diff --git a/src/components/button-toggle/button-toggle.html b/src/components/button-toggle/button-toggle.html
new file mode 100644
index 00000000000..e2e91983894
--- /dev/null
+++ b/src/components/button-toggle/button-toggle.html
@@ -0,0 +1,13 @@
+
diff --git a/src/components/button-toggle/button-toggle.scss b/src/components/button-toggle/button-toggle.scss
new file mode 100644
index 00000000000..09846d13a35
--- /dev/null
+++ b/src/components/button-toggle/button-toggle.scss
@@ -0,0 +1,46 @@
+@import "elevation";
+@import "default-theme";
+@import "palette";
+@import "mixins";
+
+$md-button-toggle-padding: 0 16px !default;
+$md-button-toggle-line-height: 36px !default;
+$md-button-toggle-border-radius: 3px !default;
+
+md-button-toggle-group {
+ @include md-elevation(2);
+ position: relative;
+ display: inline-flex;
+ border-radius: $md-button-toggle-border-radius;
+ cursor: pointer;
+ white-space: nowrap;
+}
+
+
+.md-button-toggle-checked .md-button-toggle-label-content {
+ background-color: md-color($md-grey, 300);
+}
+
+.md-button-toggle-disabled .md-button-toggle-label-content {
+ background-color: md-color($md-foreground, disabled);
+ cursor: not-allowed;
+}
+
+md-button-toggle {
+ white-space: nowrap;
+}
+
+.md-button-toggle-input {
+ @include md-visually-hidden;
+}
+
+.md-button-toggle-label-content {
+ display: inline-block;
+ line-height: $md-button-toggle-line-height;
+ padding: $md-button-toggle-padding;
+ cursor: pointer;
+}
+
+.md-button-toggle-label-content > * {
+ vertical-align: middle;
+}
diff --git a/src/components/button-toggle/button-toggle.spec.ts b/src/components/button-toggle/button-toggle.spec.ts
new file mode 100644
index 00000000000..12539582b8d
--- /dev/null
+++ b/src/components/button-toggle/button-toggle.spec.ts
@@ -0,0 +1,341 @@
+import {
+ it,
+ describe,
+ beforeEach,
+ beforeEachProviders,
+ inject,
+ async,
+ fakeAsync,
+ tick,
+} from '@angular/core/testing';
+import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
+import {Component, DebugElement, provide} from '@angular/core';
+import {By} from '@angular/platform-browser';
+import {
+ MD_BUTTON_TOGGLE_DIRECTIVES,
+ MdButtonToggleGroup,
+ MdButtonToggle,
+ MdButtonToggleGroupMultiple
+} from './button-toggle';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
+
+
+describe('MdButtonToggle', () => {
+ let builder: TestComponentBuilder;
+ let dispatcher: MdUniqueSelectionDispatcher;
+
+ beforeEachProviders(() => [
+ provide(MdUniqueSelectionDispatcher, {useFactory: () => {
+ dispatcher = new MdUniqueSelectionDispatcher();
+ return dispatcher;
+ }})
+ ]);
+
+ beforeEach(inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
+ builder = tcb;
+ }));
+
+ describe('inside of an exclusive selection group', () => {
+ let fixture: ComponentFixture;
+ let groupDebugElement: DebugElement;
+ let groupNativeElement: HTMLElement;
+ let buttonToggleDebugElements: DebugElement[];
+ let buttonToggleNativeElements: HTMLElement[];
+ let groupInstance: MdButtonToggleGroup;
+ let buttonToggleInstances: MdButtonToggle[];
+ let testComponent: ButtonTogglesInsideButtonToggleGroup;
+
+ beforeEach(async(() => {
+ builder.createAsync(ButtonTogglesInsideButtonToggleGroup).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);
+
+ buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
+ buttonToggleNativeElements =
+ buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
+ buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
+ });
+ }));
+
+ it('should set individual button toggle names based on the group name', () => {
+ expect(groupInstance.name).toBeTruthy();
+ for (let buttonToggle of buttonToggleInstances) {
+ expect(buttonToggle.name).toBe(groupInstance.name);
+ }
+ });
+
+ it('should disable click interactions when the group is disabled', () => {
+ testComponent.isGroupDisabled = true;
+ fixture.detectChanges();
+
+ buttonToggleNativeElements[0].click();
+ expect(buttonToggleInstances[0].checked).toBe(false);
+ });
+
+ it('should update the group value when one of the toggles changes', () => {
+ expect(groupInstance.value).toBeFalsy();
+ let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement;
+
+ nativeCheckboxLabel.click();
+ fixture.detectChanges();
+
+ expect(groupInstance.value).toBe('test1');
+ expect(groupInstance.selected).toBe(buttonToggleInstances[0]);
+ });
+
+ it('should update the group and toggles when one of the button toggles is clicked', () => {
+ expect(groupInstance.value).toBeFalsy();
+ let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement;
+
+ nativeCheckboxLabel.click();
+ fixture.detectChanges();
+
+ expect(groupInstance.value).toBe('test1');
+ expect(groupInstance.selected).toBe(buttonToggleInstances[0]);
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ expect(buttonToggleInstances[1].checked).toBe(false);
+
+ let nativeCheckboxLabel2 = buttonToggleDebugElements[1].query(By.css('label')).nativeElement;
+
+ nativeCheckboxLabel2.click();
+ fixture.detectChanges();
+
+ expect(groupInstance.value).toBe('test2');
+ expect(groupInstance.selected).toBe(buttonToggleInstances[1]);
+ expect(buttonToggleInstances[0].checked).toBe(false);
+ expect(buttonToggleInstances[1].checked).toBe(true);
+ });
+
+ it('should check a button toggle upon interaction with underlying native radio button', () => {
+ let nativeRadioInput = buttonToggleDebugElements[0].query(By.css('input')).nativeElement;
+
+ nativeRadioInput.click();
+ fixture.detectChanges();
+
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ expect(groupInstance.value);
+ });
+
+ it('should emit a change event from button toggles', fakeAsync(() => {
+ expect(buttonToggleInstances[0].checked).toBe(false);
+
+ let changeSpy = jasmine.createSpy('button-toggle change listener');
+ buttonToggleInstances[0].change.subscribe(changeSpy);
+
+ buttonToggleInstances[0].checked = true;
+ fixture.detectChanges();
+ tick();
+ expect(changeSpy).toHaveBeenCalled();
+
+ buttonToggleInstances[0].checked = false;
+ fixture.detectChanges();
+ tick();
+ expect(changeSpy).toHaveBeenCalledTimes(2);
+ }));
+
+ it('should emit a change event from the button toggle group', fakeAsync(() => {
+ expect(groupInstance.value).toBeFalsy();
+
+ let changeSpy = jasmine.createSpy('button-toggle-group change listener');
+ groupInstance.change.subscribe(changeSpy);
+
+ groupInstance.value = 'test1';
+ fixture.detectChanges();
+ tick();
+ expect(changeSpy).toHaveBeenCalled();
+
+ groupInstance.value = 'test2';
+ fixture.detectChanges();
+ tick();
+ expect(changeSpy).toHaveBeenCalledTimes(2);
+ }));
+
+ it('should update the group and button toggles when updating the group value', () => {
+ expect(groupInstance.value).toBeFalsy();
+
+ testComponent.groupValue = 'test1';
+ fixture.detectChanges();
+
+ expect(groupInstance.value).toBe('test1');
+ expect(groupInstance.selected).toBe(buttonToggleInstances[0]);
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ expect(buttonToggleInstances[1].checked).toBe(false);
+
+ testComponent.groupValue = 'test2';
+ fixture.detectChanges();
+
+ expect(groupInstance.value).toBe('test2');
+ expect(groupInstance.selected).toBe(buttonToggleInstances[1]);
+ expect(buttonToggleInstances[0].checked).toBe(false);
+ expect(buttonToggleInstances[1].checked).toBe(true);
+ });
+
+ it('should deselect all of the checkboxes when the group value is cleared', () => {
+ buttonToggleInstances[0].checked = true;
+
+ expect(groupInstance.value).toBeTruthy();
+
+ groupInstance.value = null;
+
+ expect(buttonToggleInstances.every(toggle => !toggle.checked)).toBe(true);
+ });
+ });
+
+ describe('inside of a multiple selection group', () => {
+ let fixture: ComponentFixture;
+ let groupDebugElement: DebugElement;
+ let groupNativeElement: HTMLElement;
+ let buttonToggleDebugElements: DebugElement[];
+ let buttonToggleNativeElements: HTMLElement[];
+ let groupInstance: MdButtonToggleGroupMultiple;
+ let buttonToggleInstances: MdButtonToggle[];
+ let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple;
+
+ beforeEach(async(() => {
+ builder.createAsync(ButtonTogglesInsideButtonToggleGroupMultiple).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+
+ groupDebugElement = fixture.debugElement.query(By.directive(MdButtonToggleGroupMultiple));
+ groupNativeElement = groupDebugElement.nativeElement;
+ groupInstance = groupDebugElement.injector.get(MdButtonToggleGroupMultiple);
+
+ buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MdButtonToggle));
+ buttonToggleNativeElements =
+ buttonToggleDebugElements.map(debugEl => debugEl.nativeElement);
+ buttonToggleInstances = buttonToggleDebugElements.map(debugEl => debugEl.componentInstance);
+ });
+ }));
+
+ it('should disable click interactions when the group is disabled', () => {
+ testComponent.isGroupDisabled = true;
+ fixture.detectChanges();
+
+ buttonToggleNativeElements[0].click();
+ expect(buttonToggleInstances[0].checked).toBe(false);
+ });
+
+ it('should check a button toggle when clicked', () => {
+ expect(buttonToggleInstances.every(buttonToggle => !buttonToggle.checked)).toBe(true);
+
+ let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement;
+
+ nativeCheckboxLabel.click();
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ });
+
+ it('should allow for multiple toggles to be selected', () => {
+ buttonToggleInstances[0].checked = true;
+ fixture.detectChanges();
+ expect(buttonToggleInstances[0].checked).toBe(true);
+
+ buttonToggleInstances[1].checked = true;
+ fixture.detectChanges();
+ expect(buttonToggleInstances[1].checked).toBe(true);
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ });
+
+ it('should check a button toggle upon interaction with underlying native checkbox', () => {
+ let nativeCheckboxInput = buttonToggleDebugElements[0].query(By.css('input')).nativeElement;
+
+ nativeCheckboxInput.click();
+ fixture.detectChanges();
+
+ expect(buttonToggleInstances[0].checked).toBe(true);
+ });
+
+ it('should deselect a button toggle when selected twice', () => {
+ buttonToggleNativeElements[0].click();
+ fixture.detectChanges();
+
+ buttonToggleNativeElements[0].click();
+ fixture.detectChanges();
+
+ expect(buttonToggleInstances[0].checked).toBe(false);
+ });
+ });
+
+ describe('as standalone', () => {
+ let fixture: ComponentFixture;
+ let buttonToggleDebugElement: DebugElement;
+ let buttonToggleNativeElement: HTMLElement;
+ let buttonToggleInstance: MdButtonToggle;
+ let testComponent: StandaloneButtonToggle;
+
+ beforeEach(async(() => {
+ builder.createAsync(StandaloneButtonToggle).then(f => {
+ fixture = f;
+ fixture.detectChanges();
+
+ testComponent = fixture.debugElement.componentInstance;
+
+ buttonToggleDebugElement = fixture.debugElement.query(By.directive(MdButtonToggle));
+ buttonToggleNativeElement = buttonToggleDebugElement.nativeElement;
+ buttonToggleInstance = buttonToggleDebugElement.componentInstance;
+ });
+ }));
+
+ it('should toggle when clicked', () => {
+ let nativeCheckboxLabel = buttonToggleDebugElement.query(By.css('label')).nativeElement;
+
+ nativeCheckboxLabel.click();
+
+ fixture.detectChanges();
+
+ expect(buttonToggleInstance.checked).toBe(true);
+
+ nativeCheckboxLabel.click();
+ fixture.detectChanges();
+
+ expect(buttonToggleInstance.checked).toBe(false);
+ });
+ });
+});
+
+
+@Component({
+ directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
+ template: `
+
+ Test1
+ Test2
+ Test3
+
+ `
+})
+class ButtonTogglesInsideButtonToggleGroup {
+ isGroupDisabled: boolean = false;
+ groupValue: string = null;
+}
+
+@Component({
+ directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
+ template: `
+
+ Eggs
+ Flour
+ Sugar
+
+ `
+})
+class ButtonTogglesInsideButtonToggleGroupMultiple {
+ isGroupDisabled: boolean = false;
+}
+
+@Component({
+ directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
+ template: `
+ Yes
+ `
+})
+class StandaloneButtonToggle { }
diff --git a/src/components/button-toggle/button-toggle.ts b/src/components/button-toggle/button-toggle.ts
new file mode 100644
index 00000000000..270991aa367
--- /dev/null
+++ b/src/components/button-toggle/button-toggle.ts
@@ -0,0 +1,335 @@
+import {
+ Component,
+ ContentChildren,
+ Directive,
+ EventEmitter,
+ HostBinding,
+ Input,
+ OnInit,
+ Optional,
+ Output,
+ QueryList,
+ ViewEncapsulation,
+ forwardRef
+} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
+import {BooleanFieldValue} from '@angular2-material/core/annotations/field-value';
+
+export type ToggleType = 'checkbox' | 'radio';
+
+
+var _uniqueIdCounter = 0;
+
+/** A simple change event emitted by either MdButtonToggle or MdButtonToggleGroup. */
+export class MdButtonToggleChange {
+ source: MdButtonToggle;
+ value: any;
+}
+
+/** Exclusive selection button toggle group that behaves like a radio-button group. */
+@Directive({
+ selector: 'md-button-toggle-group:not([multiple])',
+ host: {
+ 'role': 'radiogroup',
+ },
+})
+export class MdButtonToggleGroup {
+ /** The value for the button toggle group. Should match currently selected button toggle. */
+ private _value: any = null;
+
+ /** The HTML name attribute applied to toggles in this group. */
+ private _name: string = `md-radio-group-${_uniqueIdCounter++}`;
+
+ /** Disables all toggles in the group. */
+ private _disabled: boolean = null;
+
+ /** The currently selected button toggle, should match the value. */
+ private _selected: MdButtonToggle = null;
+
+ /** Event emitted when the group's value changes. */
+ private _change: EventEmitter = new EventEmitter();
+ @Output() get change(): Observable {
+ return this._change.asObservable();
+ }
+
+ /** Child button toggle buttons. */
+ @ContentChildren(forwardRef(() => MdButtonToggle))
+ private _buttonToggles: QueryList = null;
+
+ @Input()
+ get name(): string {
+ return this._name;
+ }
+
+ set name(value: string) {
+ this._name = value;
+
+ this._updateButtonToggleNames();
+ }
+
+ @Input()
+ @BooleanFieldValue()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+
+ set disabled(value) {
+ this._disabled = (value != null && value !== false) ? true : null;
+ }
+
+ @Input()
+ get value(): any {
+ return this._value;
+ }
+
+ set value(newValue: any) {
+ if (this._value != newValue) {
+ this._value = newValue;
+
+ this._updateSelectedButtonToggleFromValue();
+ this._emitChangeEvent();
+ }
+ }
+
+ @Input()
+ get selected() {
+ return this._selected;
+ }
+
+ set selected(selected: MdButtonToggle) {
+ this._selected = selected;
+ this.value = selected ? selected.value : null;
+
+ if (selected && !selected.checked) {
+ selected.checked = true;
+ }
+ }
+
+ private _updateButtonToggleNames(): void {
+ (this._buttonToggles || []).forEach((toggle) => {
+ toggle.name = this._name;
+ });
+ }
+
+ // TODO: Refactor into shared code with radio.
+ private _updateSelectedButtonToggleFromValue(): void {
+ let isAlreadySelected = this._selected != null && this._selected.value == this._value;
+
+ if (this._buttonToggles != null && !isAlreadySelected) {
+ let matchingButtonToggle = this._buttonToggles.filter(
+ buttonToggle => buttonToggle.value == this._value)[0];
+
+ if (matchingButtonToggle) {
+ this.selected = matchingButtonToggle;
+ } else if (this.value == null) {
+ this.selected = null;
+ this._buttonToggles.forEach(buttonToggle => {buttonToggle.checked = false; });
+ }
+ }
+ }
+
+ /** Dispatch change event with current selection and group value. */
+ private _emitChangeEvent(): void {
+ let event = new MdButtonToggleChange();
+ event.source = this._selected;
+ event.value = this._value;
+ this._change.emit(event);
+ }
+}
+
+/** Multiple selection button-toggle group. */
+@Directive({
+ selector: 'md-button-toggle-group[multiple]',
+})
+export class MdButtonToggleGroupMultiple {
+ /** Disables all toggles in the group. */
+ private _disabled: boolean = null;
+
+ @Input()
+ get disabled(): boolean {
+ return this._disabled;
+ }
+
+ set disabled(value) {
+ this._disabled = (value != null && value !== false) ? true : null;
+ }
+}
+
+@Component({
+ moduleId: module.id,
+ selector: 'md-button-toggle',
+ templateUrl: 'button-toggle.html',
+ styleUrls: ['button-toggle.css'],
+ encapsulation: ViewEncapsulation.None,
+})
+export class MdButtonToggle implements OnInit {
+ /** Whether or not this button toggle is checked. */
+ private _checked: boolean = false;
+
+ /**
+ * Type of the button toggle. Either 'radio' or 'checkbox'.
+ * @internal
+ */
+ type: ToggleType;
+
+ /** The unique ID for this button toggle. */
+ @HostBinding()
+ @Input()
+ id: string;
+
+ /** HTML's 'name' attribute used to group radios for unique selection. */
+ @Input()
+ name: string;
+
+ /** Whether or not this button toggle is disabled. */
+ private _disabled: boolean = null;
+
+ /** Value assigned to this button toggle. */
+ private _value: any = null;
+
+ /** Whether or not the button toggle is a single selection. */
+ private _isSingleSelector: boolean = null;
+
+ /** The parent button toggle group (exclusive selection). Optional. */
+ buttonToggleGroup: MdButtonToggleGroup;
+
+ /** The parent button toggle group (multiple selection). Optional. */
+ buttonToggleGroupMultiple: MdButtonToggleGroupMultiple;
+
+ /** Event emitted when the group value changes. */
+ private _change: EventEmitter = new EventEmitter();
+ @Output() get change(): Observable {
+ return this._change.asObservable();
+ }
+
+ constructor(@Optional() toggleGroup: MdButtonToggleGroup,
+ @Optional() toggleGroupMultiple: MdButtonToggleGroupMultiple,
+ public buttonToggleDispatcher: MdUniqueSelectionDispatcher) {
+ this.buttonToggleGroup = toggleGroup;
+
+ this.buttonToggleGroupMultiple = toggleGroupMultiple;
+
+ if (this.buttonToggleGroup) {
+ buttonToggleDispatcher.listen((id: string, name: string) => {
+ if (id != this.id && name == this.name) {
+ this.checked = false;
+ }
+ });
+
+ this.type = 'radio';
+ this.name = this.buttonToggleGroup.name;
+ this._isSingleSelector = true;
+ } else {
+ // Even if there is no group at all, treat the button toggle as a checkbox so it can be
+ // toggled on or off.
+ this.type = 'checkbox';
+ this._isSingleSelector = false;
+ }
+ }
+
+ /** @internal */
+ ngOnInit() {
+ if (this.id == null) {
+ this.id = `md-button-toggle-${_uniqueIdCounter++}`;
+ }
+
+ if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
+ this._checked = true;
+ }
+
+ }
+
+ get inputId(): string {
+ return `${this.id}-input`;
+ }
+
+ @HostBinding('class.md-button-toggle-checked')
+ @Input()
+ get checked(): boolean {
+ return this._checked;
+ }
+
+ set checked(newCheckedState: boolean) {
+ if (this._isSingleSelector) {
+ if (newCheckedState) {
+ // Notify all button toggles with the same name (in the same group) to un-check.
+ this.buttonToggleDispatcher.notify(this.id, this.name);
+ }
+
+ if (newCheckedState != this._checked) {
+ this._emitChangeEvent();
+ }
+ }
+
+ this._checked = newCheckedState;
+
+ if (newCheckedState && this._isSingleSelector && this.buttonToggleGroup.value != this.value) {
+ this.buttonToggleGroup.selected = this;
+ }
+ }
+
+ /** MdButtonToggleGroup reads this to assign its own value. */
+ @Input()
+ get value(): any {
+ return this._value;
+ }
+
+ set value(value: any) {
+ if (this._value != value) {
+ if (this.buttonToggleGroup != null && this.checked) {
+ this.buttonToggleGroup.value = value;
+ }
+ this._value = value;
+ }
+ }
+
+ /** Dispatch change event with current value. */
+ private _emitChangeEvent(): void {
+ let event = new MdButtonToggleChange();
+ event.source = this;
+ event.value = this._value;
+ this._change.emit(event);
+ }
+
+ @HostBinding('class.md-button-toggle-disabled')
+ @Input()
+ get disabled(): boolean {
+ return this._disabled || (this.buttonToggleGroup != null && this.buttonToggleGroup.disabled) ||
+ (this.buttonToggleGroupMultiple != null && this.buttonToggleGroupMultiple.disabled);
+ }
+
+ set disabled(value: boolean) {
+ this._disabled = (value != null && value !== false) ? true : null;
+ }
+
+ /** Toggle the state of the current button toggle. */
+ private _toggle(): void {
+ this.checked = !this.checked;
+ }
+
+ /**
+ * Checks the button toggle due to an interaction with the underlying native input.
+ * @internal
+ */
+ onInputChange(event: Event) {
+ event.stopPropagation();
+
+ if (this._isSingleSelector) {
+ // Propagate the change one-way via the group, which will in turn mark this
+ // button toggle as checked.
+ this.checked = true;
+ this.buttonToggleGroup.selected = this;
+ } else {
+ this._toggle();
+ }
+ }
+}
+
+export const MD_BUTTON_TOGGLE_DIRECTIVES = [
+ MdButtonToggleGroup,
+ MdButtonToggleGroupMultiple,
+ MdButtonToggle
+];
diff --git a/src/components/radio/radio.spec.ts b/src/components/radio/radio.spec.ts
index 565769a2cce..96405e80976 100644
--- a/src/components/radio/radio.spec.ts
+++ b/src/components/radio/radio.spec.ts
@@ -1,28 +1,30 @@
import {
- it,
- describe,
- beforeEach,
- beforeEachProviders,
- inject,
- async,
- fakeAsync,
- tick
+ it,
+ describe,
+ beforeEach,
+ beforeEachProviders,
+ inject,
+ async,
+ fakeAsync,
+ tick
} from '@angular/core/testing';
import {FORM_DIRECTIVES, NgControl} from '@angular/common';
import {TestComponentBuilder, ComponentFixture} from '@angular/compiler/testing';
import {Component, DebugElement, provide} from '@angular/core';
import {By} from '@angular/platform-browser';
import {MD_RADIO_DIRECTIVES, MdRadioGroup, MdRadioButton, MdRadioChange} from './radio';
-import {MdRadioDispatcher} from './radio_dispatcher';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
describe('MdRadio', () => {
let builder: TestComponentBuilder;
- let dispatcher: MdRadioDispatcher;
+ let dispatcher: MdUniqueSelectionDispatcher;
beforeEachProviders(() => [
- provide(MdRadioDispatcher, {useFactory: () => {
- dispatcher = new MdRadioDispatcher();
+ provide(MdUniqueSelectionDispatcher, {useFactory: () => {
+ dispatcher = new MdUniqueSelectionDispatcher();
return dispatcher;
}})
]);
diff --git a/src/components/radio/radio.ts b/src/components/radio/radio.ts
index 57ca45bfb5b..94114db1f71 100644
--- a/src/components/radio/radio.ts
+++ b/src/components/radio/radio.ts
@@ -18,11 +18,15 @@ import {
NG_VALUE_ACCESSOR,
ControlValueAccessor
} from '@angular/common';
-import {MdRadioDispatcher} from './radio_dispatcher';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
// Re-exports.
-export {MdRadioDispatcher} from './radio_dispatcher';
+export {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
@@ -273,7 +277,8 @@ export class MdRadioButton implements OnInit {
@Output()
change: EventEmitter = new EventEmitter();
- constructor(@Optional() radioGroup: MdRadioGroup, public radioDispatcher: MdRadioDispatcher) {
+ constructor(@Optional() radioGroup: MdRadioGroup,
+ public radioDispatcher: MdUniqueSelectionDispatcher) {
// Assertions. Ideally these should be stripped out by the compiler.
// TODO(jelbourn): Assert that there's no name binding AND a parent radio group.
diff --git a/src/components/radio/radio_dispatcher.ts b/src/core/coordination/unique-selection-dispatcher.ts
similarity index 62%
rename from src/components/radio/radio_dispatcher.ts
rename to src/core/coordination/unique-selection-dispatcher.ts
index cfe40eadfbe..1e6409a0d33 100644
--- a/src/components/radio/radio_dispatcher.ts
+++ b/src/core/coordination/unique-selection-dispatcher.ts
@@ -2,10 +2,10 @@ import {Injectable} from '@angular/core';
// Users of the Dispatcher never need to see this type, but TypeScript requires it to be exported.
-export type MdRadioDispatcherListener = (id: string, name: string) => void;
+export type MdUniqueSelectionDispatcherListener = (id: string, name: string) => void;
/**
- * Class for radio buttons to coordinate unique selection based on name.
+ * Class to coordinate unique selection based on name.
* Intended to be consumed as an Angular service.
* This service is needed because native radio change events are only fired on the item currently
* being selected, and we still need to uncheck the previous selection.
@@ -14,18 +14,18 @@ export type MdRadioDispatcherListener = (id: string, name: string) => void;
* less error-prone if they are simply passed through when the events occur.
*/
@Injectable()
-export class MdRadioDispatcher {
- private _listeners: MdRadioDispatcherListener[] = [];
+export class MdUniqueSelectionDispatcher {
+ private _listeners: MdUniqueSelectionDispatcherListener[] = [];
- /** Notify other radio buttons that selection for the given name has been set. */
+ /** Notify other items that selection for the given name has been set. */
notify(id: string, name: string) {
for (let listener of this._listeners) {
listener(id, name);
}
}
- /** Listen for future changes to radio button selection. */
- listen(listener: MdRadioDispatcherListener) {
+ /** Listen for future changes to item selection. */
+ listen(listener: MdUniqueSelectionDispatcherListener) {
this._listeners.push(listener);
}
}
diff --git a/src/demo-app/button-toggle/button-toggle-demo.html b/src/demo-app/button-toggle/button-toggle-demo.html
new file mode 100644
index 00000000000..982f87e5a20
--- /dev/null
+++ b/src/demo-app/button-toggle/button-toggle-demo.html
@@ -0,0 +1,39 @@
+Exclusive Selection
+
+
+
+ format_align_left
+ format_align_center
+ format_align_right
+ format_align_justify
+
+
+
+Disabled Group
+
+
+
+
+ format_bold
+
+
+ format_italic
+
+
+ format_underline
+
+
+
+
+Multiple Selection
+
+
+ Flour
+ Eggs
+ Sugar
+ Milk (disabled)
+
+
+
+Single Toggle
+Yes
diff --git a/src/demo-app/button-toggle/button-toggle-demo.ts b/src/demo-app/button-toggle/button-toggle-demo.ts
new file mode 100644
index 00000000000..4cb8335997b
--- /dev/null
+++ b/src/demo-app/button-toggle/button-toggle-demo.ts
@@ -0,0 +1,15 @@
+import {Component} from '@angular/core';
+import {MD_BUTTON_TOGGLE_DIRECTIVES} from '@angular2-material/button-toggle/button-toggle';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
+import {MdIcon} from '@angular2-material/icon/icon';
+
+@Component({
+ moduleId: module.id,
+ selector: 'button-toggle-demo',
+ templateUrl: 'button-toggle-demo.html',
+ providers: [MdUniqueSelectionDispatcher],
+ directives: [MD_BUTTON_TOGGLE_DIRECTIVES, MdIcon]
+})
+export class ButtonToggleDemo { }
diff --git a/src/demo-app/demo-app/demo-app.html b/src/demo-app/demo-app/demo-app.html
index ec92e9bea76..b3219d41312 100644
--- a/src/demo-app/demo-app/demo-app.html
+++ b/src/demo-app/demo-app/demo-app.html
@@ -2,6 +2,7 @@
Button
+ Button Toggle
Card
Checkbox
Gestures
diff --git a/src/demo-app/demo-app/demo-app.ts b/src/demo-app/demo-app/demo-app.ts
index d7e47e9490c..f7320965c4b 100644
--- a/src/demo-app/demo-app/demo-app.ts
+++ b/src/demo-app/demo-app/demo-app.ts
@@ -26,6 +26,7 @@ import {GesturesDemo} from '../gestures/gestures-demo';
import {GridListDemo} from '../grid-list/grid-list-demo';
import {TabsDemo} from '../tabs/tab-group-demo';
import {SlideToggleDemo} from '../slide-toggle/slide-toggle-demo';
+import {ButtonToggleDemo} from '../button-toggle/button-toggle-demo';
@Component({
selector: 'home',
@@ -72,6 +73,7 @@ export class Home {}
new Route({path: '/live-announcer', component: LiveAnnouncerDemo}),
new Route({path: '/gestures', component: GesturesDemo}),
new Route({path: '/grid-list', component: GridListDemo}),
- new Route({path: '/tabs', component: TabsDemo})
+ new Route({path: '/tabs', component: TabsDemo}),
+ new Route({path: '/button-toggle', component: ButtonToggleDemo}),
])
export class DemoApp { }
diff --git a/src/demo-app/radio/radio-demo.ts b/src/demo-app/radio/radio-demo.ts
index a64548b40e1..278058af1ff 100644
--- a/src/demo-app/radio/radio-demo.ts
+++ b/src/demo-app/radio/radio-demo.ts
@@ -1,13 +1,15 @@
import {Component} from '@angular/core';
import {MdRadioButton, MdRadioGroup} from '@angular2-material/radio/radio';
-import {MdRadioDispatcher} from '@angular2-material/radio/radio_dispatcher';
+import {
+ MdUniqueSelectionDispatcher
+} from '@angular2-material/core/coordination/unique-selection-dispatcher';
@Component({
moduleId: module.id,
selector: 'radio-demo',
templateUrl: 'radio-demo.html',
styleUrls: ['radio-demo.css'],
- providers: [MdRadioDispatcher],
+ providers: [MdUniqueSelectionDispatcher],
directives: [MdRadioButton, MdRadioGroup]
})
export class RadioDemo {
diff --git a/src/demo-app/system-config.ts b/src/demo-app/system-config.ts
index 53bd96fe04c..3782a58b29f 100644
--- a/src/demo-app/system-config.ts
+++ b/src/demo-app/system-config.ts
@@ -18,6 +18,7 @@ const components = [
'radio',
'sidenav',
'slide-toggle',
+ 'button-toggle',
'tabs',
'toolbar'
];