mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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
This commit is contained in:
committed by
Jeremy Elbourn
parent
e366fa0fbc
commit
013eabd672
@@ -0,0 +1,13 @@
|
||||
<label [attr.for]="inputId" class="md-button-toggle-label">
|
||||
<input #input class="md-button-toggle-input"
|
||||
[type]="type"
|
||||
[id]="inputId"
|
||||
[checked]="checked"
|
||||
[disabled]="disabled"
|
||||
[name]="name"
|
||||
(change)="onInputChange($event)">
|
||||
|
||||
<div class="md-button-toggle-label-content">
|
||||
<ng-content></ng-content>
|
||||
</div>
|
||||
</label>
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ButtonTogglesInsideButtonToggleGroup>;
|
||||
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<ButtonTogglesInsideButtonToggleGroupMultiple>;
|
||||
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<StandaloneButtonToggle>;
|
||||
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: `
|
||||
<md-button-toggle-group [disabled]="isGroupDisabled" [value]="groupValue">
|
||||
<md-button-toggle value="test1">Test1</md-button-toggle>
|
||||
<md-button-toggle value="test2">Test2</md-button-toggle>
|
||||
<md-button-toggle value="test3">Test3</md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
`
|
||||
})
|
||||
class ButtonTogglesInsideButtonToggleGroup {
|
||||
isGroupDisabled: boolean = false;
|
||||
groupValue: string = null;
|
||||
}
|
||||
|
||||
@Component({
|
||||
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
|
||||
template: `
|
||||
<md-button-toggle-group [disabled]="isGroupDisabled" multiple>
|
||||
<md-button-toggle value="eggs">Eggs</md-button-toggle>
|
||||
<md-button-toggle value="flour">Flour</md-button-toggle>
|
||||
<md-button-toggle value="sugar">Sugar</md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
`
|
||||
})
|
||||
class ButtonTogglesInsideButtonToggleGroupMultiple {
|
||||
isGroupDisabled: boolean = false;
|
||||
}
|
||||
|
||||
@Component({
|
||||
directives: [MD_BUTTON_TOGGLE_DIRECTIVES],
|
||||
template: `
|
||||
<md-button-toggle>Yes</md-button-toggle>
|
||||
`
|
||||
})
|
||||
class StandaloneButtonToggle { }
|
||||
@@ -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<MdButtonToggleChange> = new EventEmitter<MdButtonToggleChange>();
|
||||
@Output() get change(): Observable<MdButtonToggleChange> {
|
||||
return this._change.asObservable();
|
||||
}
|
||||
|
||||
/** Child button toggle buttons. */
|
||||
@ContentChildren(forwardRef(() => MdButtonToggle))
|
||||
private _buttonToggles: QueryList<MdButtonToggle> = 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<MdButtonToggleChange> = new EventEmitter<MdButtonToggleChange>();
|
||||
@Output() get change(): Observable<MdButtonToggleChange> {
|
||||
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
|
||||
];
|
||||
@@ -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;
|
||||
}})
|
||||
]);
|
||||
|
||||
@@ -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<MdRadioChange> = new EventEmitter<MdRadioChange>();
|
||||
|
||||
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.
|
||||
|
||||
|
||||
+7
-7
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<h1>Exclusive Selection</h1>
|
||||
|
||||
<section class="demo-section">
|
||||
<md-button-toggle-group name="alignment">
|
||||
<md-button-toggle value="left"><md-icon>format_align_left</md-icon></md-button-toggle>
|
||||
<md-button-toggle value="center"><md-icon>format_align_center</md-icon></md-button-toggle>
|
||||
<md-button-toggle value="right"><md-icon>format_align_right</md-icon></md-button-toggle>
|
||||
<md-button-toggle value="justify" disabled><md-icon>format_align_justify</md-icon></md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
</section>
|
||||
|
||||
<h1>Disabled Group</h1>
|
||||
|
||||
<section class="demo-section">
|
||||
<md-button-toggle-group name="checkbox" disabled>
|
||||
<md-button-toggle value="bold">
|
||||
<md-icon class="md-24">format_bold</md-icon>
|
||||
</md-button-toggle>
|
||||
<md-button-toggle value="italic">
|
||||
<md-icon class="md-24">format_italic</md-icon>
|
||||
</md-button-toggle>
|
||||
<md-button-toggle value="underline">
|
||||
<md-icon class="md-24">format_underline</md-icon>
|
||||
</md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
</section>
|
||||
|
||||
<h1>Multiple Selection</h1>
|
||||
<section class="demo-section">
|
||||
<md-button-toggle-group multiple>
|
||||
<md-button-toggle>Flour</md-button-toggle>
|
||||
<md-button-toggle>Eggs</md-button-toggle>
|
||||
<md-button-toggle>Sugar</md-button-toggle>
|
||||
<md-button-toggle disabled>Milk (disabled)</md-button-toggle>
|
||||
</md-button-toggle-group>
|
||||
</section>
|
||||
|
||||
<h1>Single Toggle</h1>
|
||||
<md-button-toggle>Yes</md-button-toggle>
|
||||
@@ -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 { }
|
||||
@@ -2,6 +2,7 @@
|
||||
<md-sidenav #start>
|
||||
<md-nav-list>
|
||||
<a md-list-item [routerLink]="['button']">Button</a>
|
||||
<a md-list-item [routerLink]="['button-toggle']">Button Toggle</a>
|
||||
<a md-list-item [routerLink]="['card']">Card</a>
|
||||
<a md-list-item [routerLink]="['checkbox']">Checkbox</a>
|
||||
<a md-list-item [routerLink]="['gestures']">Gestures</a>
|
||||
|
||||
@@ -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 { }
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -18,6 +18,7 @@ const components = [
|
||||
'radio',
|
||||
'sidenav',
|
||||
'slide-toggle',
|
||||
'button-toggle',
|
||||
'tabs',
|
||||
'toolbar'
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user