From 013eabd672147963575b4aa6726183cfd14dbec3 Mon Sep 17 00:00:00 2001 From: Ivey Padgett Date: Fri, 3 Jun 2016 13:19:03 -0700 Subject: [PATCH] feat(button-toggle): add initial button-toggle (#517) * md-toggle initial prototype * Use directives, no sass nesting, and private event emitter * Move toggle and radio dispatcher into shared unique selection dispatcher * Rename toggles to button-toggles * Update toggle to look more like radio after updates * Update child name from parent name * TSLint fixes * Misc style fixes for PR. * Update comment * Remove unused `_buttonToggles` * Use string literals for button toggle type * Move instantiation into the constructor * Add `isSingleSelection` getter * Move click event binding to component * Add missing tests to button toggles * Exclusive toggle should deselect all when group value is cleared * Button toggles should be checked when the checkbox is interacted with * Remove onClick() * Remove click event * Remove unnecessary getters and setters. * Add icons to demo app * Add missing semicolon * Change eventemitter to private in button toggle class * Get native elements in tests from debug elements * Move internal to end of comment and use BooleanFieldValue * Fix typo in test description. * vertical align middle all children in button toggle --- .../button-toggle/button-toggle.html | 13 + .../button-toggle/button-toggle.scss | 46 +++ .../button-toggle/button-toggle.spec.ts | 341 ++++++++++++++++++ src/components/button-toggle/button-toggle.ts | 335 +++++++++++++++++ src/components/radio/radio.spec.ts | 26 +- src/components/radio/radio.ts | 11 +- .../unique-selection-dispatcher.ts} | 14 +- .../button-toggle/button-toggle-demo.html | 39 ++ .../button-toggle/button-toggle-demo.ts | 15 + src/demo-app/demo-app/demo-app.html | 1 + src/demo-app/demo-app/demo-app.ts | 4 +- src/demo-app/radio/radio-demo.ts | 6 +- src/demo-app/system-config.ts | 1 + 13 files changed, 827 insertions(+), 25 deletions(-) create mode 100644 src/components/button-toggle/button-toggle.html create mode 100644 src/components/button-toggle/button-toggle.scss create mode 100644 src/components/button-toggle/button-toggle.spec.ts create mode 100644 src/components/button-toggle/button-toggle.ts rename src/{components/radio/radio_dispatcher.ts => core/coordination/unique-selection-dispatcher.ts} (62%) create mode 100644 src/demo-app/button-toggle/button-toggle-demo.html create mode 100644 src/demo-app/button-toggle/button-toggle-demo.ts diff --git a/src/components/button-toggle/button-toggle.html b/src/components/button-toggle/button-toggle.html new file mode 100644 index 000000000000..e2e919838945 --- /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 000000000000..09846d13a35b --- /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 000000000000..12539582b8d2 --- /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 000000000000..270991aa3674 --- /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 565769a2cce6..96405e80976c 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 57ca45bfb5bb..94114db1f717 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 cfe40eadfbee..1e6409a0d330 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 000000000000..982f87e5a203 --- /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 000000000000..4cb8335997b9 --- /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 ec92e9bea760..b3219d41312c 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 d7e47e9490c8..f7320965c4b0 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 a64548b40e18..278058af1ff7 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 53bd96fe04c6..3782a58b29f6 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' ];