diff --git a/src/lib/button-toggle/button-toggle-module.ts b/src/lib/button-toggle/button-toggle-module.ts index 372c973ede7..71f8d6c07ba 100644 --- a/src/lib/button-toggle/button-toggle-module.ts +++ b/src/lib/button-toggle/button-toggle-module.ts @@ -7,21 +7,14 @@ */ import {NgModule} from '@angular/core'; -import {MatButtonToggleGroup, MatButtonToggleGroupMultiple, MatButtonToggle} from './button-toggle'; +import {MatButtonToggleGroup, MatButtonToggle} from './button-toggle'; import {MatCommonModule, MatRippleModule} from '@angular/material/core'; -import {UNIQUE_SELECTION_DISPATCHER_PROVIDER} from '@angular/cdk/collections'; import {A11yModule} from '@angular/cdk/a11y'; @NgModule({ imports: [MatCommonModule, MatRippleModule, A11yModule], - exports: [ - MatButtonToggleGroup, - MatButtonToggleGroupMultiple, - MatButtonToggle, - MatCommonModule, - ], - declarations: [MatButtonToggleGroup, MatButtonToggleGroupMultiple, MatButtonToggle], - providers: [UNIQUE_SELECTION_DISPATCHER_PROVIDER] + exports: [MatCommonModule, MatButtonToggleGroup, MatButtonToggle], + declarations: [MatButtonToggleGroup, MatButtonToggle], }) export class MatButtonToggleModule {} diff --git a/src/lib/button-toggle/button-toggle.spec.ts b/src/lib/button-toggle/button-toggle.spec.ts index 27703fcae2a..ba34ae6fd28 100644 --- a/src/lib/button-toggle/button-toggle.spec.ts +++ b/src/lib/button-toggle/button-toggle.spec.ts @@ -1,26 +1,19 @@ -import { - async, - fakeAsync, - tick, - ComponentFixture, - TestBed, -} from '@angular/core/testing'; +import {fakeAsync, tick, ComponentFixture, TestBed} from '@angular/core/testing'; import {dispatchMouseEvent} from '@angular/cdk/testing'; import {NgModel, FormsModule, ReactiveFormsModule, FormControl} from '@angular/forms'; import {Component, DebugElement} from '@angular/core'; import {By} from '@angular/platform-browser'; import { MatButtonToggleGroup, - MatButtonToggle, MatButtonToggleGroupMultiple, + MatButtonToggle, MatButtonToggleChange, MatButtonToggleModule, } from './index'; - describe('MatButtonToggle with forms', () => { - beforeEach(async(() => { + beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [MatButtonToggleModule, FormsModule, ReactiveFormsModule], declarations: [ @@ -38,7 +31,7 @@ describe('MatButtonToggle with forms', () => { let groupInstance: MatButtonToggleGroup; let testComponent: ButtonToggleGroupWithFormControl; - beforeEach(async(() => { + beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(ButtonToggleGroupWithFormControl); fixture.detectChanges(); @@ -88,7 +81,7 @@ describe('MatButtonToggle with forms', () => { let groupNgModel: NgModel; let buttonToggleLabels: HTMLElement[]; - beforeEach(async(() => { + beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(ButtonToggleGroupWithNgModel); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; @@ -139,7 +132,10 @@ describe('MatButtonToggle with forms', () => { for (let buttonToggle of buttonToggleInstances) { expect(buttonToggle.checked).toBe(groupInstance.value === buttonToggle.value); } - expect(groupInstance.selected!.value).toBe(groupInstance.value); + + const selected = groupInstance.selected as MatButtonToggle; + + expect(selected.value).toBe(groupInstance.value); }); it('should have the correct FormControl state initially and after interaction', @@ -203,7 +199,7 @@ describe('MatButtonToggle with forms', () => { describe('MatButtonToggle without forms', () => { - beforeEach(async(() => { + beforeEach(fakeAsync(() => { TestBed.configureTestingModule({ imports: [MatButtonToggleModule], declarations: [ @@ -338,7 +334,6 @@ describe('MatButtonToggle without forms', () => { let changeSpy = jasmine.createSpy('button-toggle change listener'); buttonToggleInstances[0].change.subscribe(changeSpy); - buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); @@ -412,14 +407,16 @@ describe('MatButtonToggle without forms', () => { fixture.detectChanges(); + // Note that we cast to a boolean, because the event has some circular references + // which will crash the runner when Jasmine attempts to stringify them. + expect(!!testComponent.lastEvent).toBe(false); expect(groupInstance.value).toBe('red'); - expect(testComponent.lastEvent).toBeFalsy(); groupInstance.value = 'green'; fixture.detectChanges(); + expect(!!testComponent.lastEvent).toBe(false); expect(groupInstance.value).toBe('green'); - expect(testComponent.lastEvent).toBeFalsy(); }); }); @@ -431,20 +428,19 @@ describe('MatButtonToggle without forms', () => { let buttonToggleDebugElements: DebugElement[]; let buttonToggleNativeElements: HTMLElement[]; let buttonToggleLabelElements: HTMLLabelElement[]; - let groupInstance: MatButtonToggleGroupMultiple; + let groupInstance: MatButtonToggleGroup; let buttonToggleInstances: MatButtonToggle[]; let testComponent: ButtonTogglesInsideButtonToggleGroupMultiple; - beforeEach(async(() => { + beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(ButtonTogglesInsideButtonToggleGroupMultiple); fixture.detectChanges(); testComponent = fixture.debugElement.componentInstance; - groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple)); + groupDebugElement = fixture.debugElement.query(By.directive(MatButtonToggleGroup)); groupNativeElement = groupDebugElement.nativeElement; - groupInstance = groupDebugElement.injector.get( - MatButtonToggleGroupMultiple); + groupInstance = groupDebugElement.injector.get(MatButtonToggleGroup); buttonToggleDebugElements = fixture.debugElement.queryAll(By.directive(MatButtonToggle)); buttonToggleNativeElements = buttonToggleDebugElements @@ -468,16 +464,22 @@ describe('MatButtonToggle without forms', () => { let nativeCheckboxLabel = buttonToggleDebugElements[0].query(By.css('label')).nativeElement; nativeCheckboxLabel.click(); + + expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); }); it('should allow for multiple toggles to be selected', () => { buttonToggleInstances[0].checked = true; fixture.detectChanges(); + + expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); buttonToggleInstances[1].checked = true; fixture.detectChanges(); + + expect(groupInstance.value).toEqual(['eggs', 'flour']); expect(buttonToggleInstances[1].checked).toBe(true); expect(buttonToggleInstances[0].checked).toBe(true); }); @@ -488,6 +490,7 @@ describe('MatButtonToggle without forms', () => { nativeCheckboxInput.click(); fixture.detectChanges(); + expect(groupInstance.value).toEqual(['eggs']); expect(buttonToggleInstances[0].checked).toBe(true); }); @@ -500,18 +503,23 @@ describe('MatButtonToggle without forms', () => { expect(groupNativeElement.classList).toContain('mat-button-toggle-vertical'); }); - it('should deselect a button toggle when selected twice', () => { - buttonToggleNativeElements[0].click(); + it('should deselect a button toggle when selected twice', fakeAsync(() => { + buttonToggleLabelElements[0].click(); fixture.detectChanges(); + tick(); - buttonToggleNativeElements[0].click(); + expect(buttonToggleInstances[0].checked).toBe(true); + expect(groupInstance.value).toEqual(['eggs']); + + buttonToggleLabelElements[0].click(); fixture.detectChanges(); + tick(); + expect(groupInstance.value).toEqual([]); expect(buttonToggleInstances[0].checked).toBe(false); - }); + })); it('should emit a change event for state changes', fakeAsync(() => { - expect(buttonToggleInstances[0].checked).toBe(false); let changeSpy = jasmine.createSpy('button-toggle change listener'); @@ -521,10 +529,12 @@ describe('MatButtonToggle without forms', () => { fixture.detectChanges(); tick(); expect(changeSpy).toHaveBeenCalled(); + expect(groupInstance.value).toEqual(['eggs']); buttonToggleLabelElements[0].click(); fixture.detectChanges(); tick(); + expect(groupInstance.value).toEqual([]); // The default browser behavior is to emit an event, when the value was set // to false. That's because the current input type is set to `checkbox` when @@ -532,6 +542,16 @@ describe('MatButtonToggle without forms', () => { expect(changeSpy).toHaveBeenCalledTimes(2); })); + it('should throw when attempting to assign a non-array value', () => { + expect(() => { + groupInstance.value = 'not-an-array'; + }).toThrowError(/Value must be an array/); + }); + + it('should be able to query for the deprecated `MatButtonToggleGroupMultiple`', () => { + expect(fixture.debugElement.query(By.directive(MatButtonToggleGroupMultiple))).toBeTruthy(); + }); + }); describe('as standalone', () => { @@ -541,7 +561,7 @@ describe('MatButtonToggle without forms', () => { let buttonToggleLabelElement: HTMLLabelElement; let buttonToggleInstance: MatButtonToggle; - beforeEach(async(() => { + beforeEach(fakeAsync(() => { fixture = TestBed.createComponent(StandaloneButtonToggle); fixture.detectChanges(); @@ -551,7 +571,7 @@ describe('MatButtonToggle without forms', () => { buttonToggleInstance = buttonToggleDebugElement.componentInstance; })); - it('should toggle when clicked', () => { + it('should toggle when clicked', fakeAsync(() => { buttonToggleLabelElement.click(); fixture.detectChanges(); @@ -562,7 +582,7 @@ describe('MatButtonToggle without forms', () => { fixture.detectChanges(); expect(buttonToggleInstance.checked).toBe(false); - }); + })); it('should emit a change event for state changes', fakeAsync(() => { diff --git a/src/lib/button-toggle/button-toggle.ts b/src/lib/button-toggle/button-toggle.ts index 2f47ef7abc9..8d7e9fae9e6 100644 --- a/src/lib/button-toggle/button-toggle.ts +++ b/src/lib/button-toggle/button-toggle.ts @@ -8,8 +8,8 @@ import {FocusMonitor} from '@angular/cdk/a11y'; import {coerceBooleanProperty} from '@angular/cdk/coercion'; -import {UniqueSelectionDispatcher} from '@angular/cdk/collections'; import { + AfterContentInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, @@ -19,7 +19,6 @@ import { EventEmitter, forwardRef, Input, - OnDestroy, OnInit, Optional, Output, @@ -34,6 +33,7 @@ import { mixinDisabled, mixinDisableRipple } from '@angular/material/core'; +import {SelectionModel} from '@angular/cdk/collections'; /** Acceptable types for a button toggle. */ export type ToggleType = 'checkbox' | 'radio'; @@ -54,30 +54,52 @@ export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = { multi: true }; +/** + * @deprecated Use `MatButtonToggleGroup` instead. + * @deletion-target 7.0.0 + */ +export class MatButtonToggleGroupMultiple {} + let _uniqueIdCounter = 0; /** Change event object emitted by MatButtonToggle. */ export class MatButtonToggleChange { - /** The MatButtonToggle that emits the event. */ - source: MatButtonToggle | null; - /** The value assigned to the MatButtonToggle. */ - value: any; + constructor( + /** The MatButtonToggle that emits the event. */ + public source: MatButtonToggle, + + /** The value assigned to the MatButtonToggle. */ + public value: any) {} } /** Exclusive selection button toggle group that behaves like a radio-button group. */ @Directive({ - selector: 'mat-button-toggle-group:not([multiple])', - providers: [MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR], + selector: 'mat-button-toggle-group', + providers: [ + MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR, + {provide: MatButtonToggleGroupMultiple, useExisting: MatButtonToggleGroup}, + ], inputs: ['disabled'], host: { - 'role': 'radiogroup', + '[attr.role]': 'multiple ? "group" : "radiogroup"', 'class': 'mat-button-toggle-group', '[class.mat-button-toggle-vertical]': 'vertical' }, exportAs: 'matButtonToggleGroup', }) -export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase - implements ControlValueAccessor, CanDisable { +export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase implements + ControlValueAccessor, CanDisable, OnInit, AfterContentInit { + + private _vertical = false; + private _multiple = false; + private _selectionModel: SelectionModel; + + /** + * Used for storing a value temporarily, if it is assigned + * before the button toggles are initialized. + */ + private _tempValue: any; + /** * The method to be called in order to update ngModel. * Now `ngModel` binding is not supported in multiple selection mode. @@ -95,27 +117,35 @@ export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase get name(): string { return this._name; } set name(value: string) { this._name = value; - this._updateButtonToggleNames(); + + if (this._buttonToggles) { + this._buttonToggles.forEach(toggle => toggle.name = this._name); + } } - private _name: string = `mat-button-toggle-group-${_uniqueIdCounter++}`; + private _name = `mat-button-toggle-group-${_uniqueIdCounter++}`; /** Whether the toggle group is vertical. */ @Input() get vertical(): boolean { return this._vertical; } - set vertical(value: boolean) { this._vertical = coerceBooleanProperty(value); } - private _vertical: boolean = false; + set vertical(value: boolean) { + this._vertical = coerceBooleanProperty(value); + } /** Value of the toggle group. */ @Input() - get value(): any { return this._value; } - set value(value: any) { - if (this._value != value) { - this._value = value; - this.valueChange.emit(value); - this._updateSelectedButtonToggleFromValue(); + get value(): any { + const selected = this._selectionModel ? this._selectionModel.selected : []; + + if (this.multiple) { + return selected.map(toggle => toggle.value); } + + return selected[0] ? selected[0].value : undefined; + } + set value(newValue: any) { + this._setSelectionByValue(newValue); + this.valueChange.emit(this.value); } - private _value: any = null; /** * Event that emits whenever the value of the group changes. @@ -124,18 +154,34 @@ export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase */ @Output() readonly valueChange = new EventEmitter(); - /** The currently selected button toggle, should match the value. */ + /** + * Selected button toggles in the group. + * @deprecated + * @deletion-target 6.0.0 + */ @Input() - get selected(): MatButtonToggle | null { return this._selected; } - set selected(selected: MatButtonToggle | null) { - this._selected = selected; - this.value = selected ? selected.value : null; + get selected() { + const selected = this._selectionModel.selected; + return this.multiple ? selected : (selected[0] || null); + } + set selected(selected: MatButtonToggle | MatButtonToggle[] | null) { + if (this._buttonToggles) { + this._clearSelection(); - if (selected && !selected.checked) { - selected.checked = true; + if (Array.isArray(selected)) { + selected.forEach(toggle => toggle.checked = true); + } else if (selected) { + selected.checked = true; + } } } - private _selected: MatButtonToggle | null = null; + + /** Whether multiple button toggles can be selected. */ + @Input() + get multiple(): boolean { return this._multiple; } + set multiple(value: boolean) { + this._multiple = coerceBooleanProperty(value); + } /** Event emitted when the group's value changes. */ @Output() readonly change: EventEmitter = @@ -145,43 +191,25 @@ export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase super(); } - private _updateButtonToggleNames(): void { - if (this._buttonToggles) { - this._buttonToggles.forEach((toggle) => { - toggle.name = this._name; - }); + ngOnInit() { + this._selectionModel = new SelectionModel(this.multiple, undefined, false); + } + + ngAfterContentInit() { + // If there was an attempt to assign a value before init, use it to set the + // initial selection, otherwise check the `checked` state of the toggles. + if (typeof this._tempValue !== 'undefined') { + this._setSelectionByValue(this._tempValue); + this._tempValue = undefined; + } else { + this._selectionModel.select(...this._buttonToggles.filter(toggle => toggle.checked)); } } - // 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. */ - _emitChangeEvent(): void { - let event = new MatButtonToggleChange(); - event.source = this._selected; - event.value = this._value; - this._controlValueAccessorChangeFn(event.value); - this.change.emit(event); - } - - // Implemented as part of ControlValueAccessor. + /** + * Sets the model value. Implemented as part of ControlValueAccessor. + * @param value Value to be set to the model. + */ writeValue(value: any) { this.value = value; this._changeDetector.markForCheck(); @@ -200,34 +228,93 @@ export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase // Implemented as part of ControlValueAccessor. setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; - this._markButtonTogglesForCheck(); - } - private _markButtonTogglesForCheck() { if (this._buttonToggles) { - this._buttonToggles.forEach((toggle) => toggle._markForCheck()); + this._buttonToggles.forEach(toggle => toggle._markForCheck()); } } -} -/** Multiple selection button-toggle group. `ngModel` is not supported in this mode. */ -@Directive({ - selector: 'mat-button-toggle-group[multiple]', - exportAs: 'matButtonToggleGroup', - inputs: ['disabled'], - host: { - 'class': 'mat-button-toggle-group', - '[class.mat-button-toggle-vertical]': 'vertical', - 'role': 'group' + /** Dispatch change event with current selection and group value. */ + _emitChangeEvent(): void { + const selected = this.selected; + const source = Array.isArray(selected) ? selected[selected.length - 1] : selected; + const event = new MatButtonToggleChange(source!, this.value); + this._controlValueAccessorChangeFn(event.value); + this.change.emit(event); + } + + /** + * Syncs a button toggle's selected state with the model value. + * @param toggle Toggle to be synced. + * @param select Whether the toggle should be selected. + * @param isUserInput Whether the change was a result of a user interaction. + */ + _syncButtonToggle(toggle: MatButtonToggle, select: boolean, isUserInput = false) { + // Deselect the currently-selected toggle, if we're in single-selection + // mode and the button being toggled isn't selected at the moment. + if (!this.multiple && this.selected && !toggle.checked) { + (this.selected as MatButtonToggle).checked = false; + } + + if (select) { + this._selectionModel.select(toggle); + } else { + this._selectionModel.deselect(toggle); + } + + // Only emit the change event for user input. + if (isUserInput) { + this._emitChangeEvent(); + } + + // Note: we emit this one no matter whether it was a user interaction, because + // it is used by Angular to sync up the two-way data binding. + this.valueChange.emit(this.value); + } + + /** Checks whether a button toggle is selected. */ + _isSelected(toggle: MatButtonToggle) { + return this._selectionModel.isSelected(toggle); + } + + /** Updates the selection state of the toggles in the group based on a value. */ + private _setSelectionByValue(value: any|any[]) { + // If the toggles haven't been initialized yet, save the value for later. + if (!this._buttonToggles) { + this._tempValue = value; + return; + } + + if (this.multiple && value) { + if (!Array.isArray(value)) { + throw Error('Value must be an array in multiple-selection mode.'); + } + + this._clearSelection(); + value.forEach((currentValue: any) => this._selectValue(currentValue)); + } else { + this._clearSelection(); + this._selectValue(value); + } + } + + /** Clears the selected toggles. */ + private _clearSelection() { + this._selectionModel.clear(); + this._buttonToggles.forEach(toggle => toggle.checked = false); + } + + /** Selects a value if there's a toggle that corresponds to it. */ + private _selectValue(value: any) { + const correspondingOption = this._buttonToggles.find(toggle => { + return toggle.value != null && toggle.value === value; + }); + + if (correspondingOption) { + correspondingOption.checked = true; + this._selectionModel.select(correspondingOption); + } } -}) -export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase - implements CanDisable { - /** Whether the toggle group is vertical. */ - @Input() - get vertical(): boolean { return this._vertical; } - set vertical(value: boolean) { this._vertical = coerceBooleanProperty(value); } - private _vertical: boolean = false; } // Boilerplate for applying mixins to the MatButtonToggle class. @@ -246,15 +333,17 @@ export const _MatButtonToggleMixinBase = mixinDisableRipple(MatButtonToggleBase) changeDetection: ChangeDetectionStrategy.OnPush, inputs: ['disableRipple'], host: { - '[class.mat-button-toggle-standalone]': '!buttonToggleGroup && !buttonToggleGroupMultiple', + '[class.mat-button-toggle-standalone]': '!buttonToggleGroup', '[class.mat-button-toggle-checked]': 'checked', '[class.mat-button-toggle-disabled]': 'disabled', 'class': 'mat-button-toggle', '[attr.id]': 'id', } }) -export class MatButtonToggle extends _MatButtonToggleMixinBase - implements OnInit, OnDestroy, CanDisableRipple { +export class MatButtonToggle extends _MatButtonToggleMixinBase implements OnInit, CanDisableRipple { + + private _isSingleSelector = false; + private _checked = false; /** * Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will @@ -270,20 +359,11 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase /** Type of the button toggle. Either 'radio' or 'checkbox'. */ _type: ToggleType; - /** Whether or not the button toggle is a single selection. */ - private _isSingleSelector: boolean = false; - - /** Unregister function for _buttonToggleDispatcher */ - private _removeUniqueSelectionListener: () => void = () => {}; - @ViewChild('input') _inputElement: ElementRef; /** The parent button toggle group (exclusive selection). Optional. */ buttonToggleGroup: MatButtonToggleGroup; - /** The parent button toggle group (multiple selection). Optional. */ - buttonToggleGroupMultiple: MatButtonToggleGroupMultiple; - /** Unique ID for the underlying `input` element. */ get inputId(): string { return `${this.id}-input`; } @@ -293,42 +373,32 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase /** HTML's 'name' attribute used to group radios for unique selection. */ @Input() name: string; + /** MatButtonToggleGroup reads this to assign its own value. */ + @Input() value: any; + /** Whether the button is checked. */ @Input() - get checked(): boolean { return this._checked; } + get checked(): boolean { + return this.buttonToggleGroup ? this.buttonToggleGroup._isSelected(this) : this._checked; + } set checked(value: boolean) { - if (this._isSingleSelector && value) { - // Notify all button toggles with the same name (in the same group) to un-check. - this._buttonToggleDispatcher.notify(this.id, this.name); + const newValue = coerceBooleanProperty(value); + + if (newValue !== this._checked) { + this._checked = newValue; + + if (this.buttonToggleGroup) { + this.buttonToggleGroup._syncButtonToggle(this, this._checked); + } + this._changeDetectorRef.markForCheck(); } - - this._checked = value; - - if (value && this._isSingleSelector && this.buttonToggleGroup.value != this.value) { - this.buttonToggleGroup.selected = this; - } } - private _checked: boolean = false; - - /** MatButtonToggleGroup 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; - } - } - private _value: any = null; /** Whether the button is disabled. */ @Input() get disabled(): boolean { - return this._disabled || (this.buttonToggleGroup != null && this.buttonToggleGroup.disabled) || - (this.buttonToggleGroupMultiple != null && this.buttonToggleGroupMultiple.disabled); + return this._disabled || (this.buttonToggleGroup && this.buttonToggleGroup.disabled); } set disabled(value: boolean) { this._disabled = coerceBooleanProperty(value); } private _disabled: boolean = false; @@ -338,44 +408,23 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase new EventEmitter(); constructor(@Optional() toggleGroup: MatButtonToggleGroup, - @Optional() toggleGroupMultiple: MatButtonToggleGroupMultiple, private _changeDetectorRef: ChangeDetectorRef, - private _buttonToggleDispatcher: UniqueSelectionDispatcher, private _elementRef: ElementRef, private _focusMonitor: FocusMonitor) { super(); this.buttonToggleGroup = toggleGroup; - this.buttonToggleGroupMultiple = toggleGroupMultiple; - - if (this.buttonToggleGroup) { - this._removeUniqueSelectionListener = - _buttonToggleDispatcher.listen((id: string, name: string) => { - if (id != this.id && name == this.name) { - this.checked = false; - this._changeDetectorRef.markForCheck(); - } - }); - - 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; - } } ngOnInit() { - if (this.id == null) { - this.id = `mat-button-toggle-${_uniqueIdCounter++}`; + this._isSingleSelector = this.buttonToggleGroup && !this.buttonToggleGroup.multiple; + this._type = this._isSingleSelector ? 'radio' : 'checkbox'; + this.id = this.id || `mat-button-toggle-${_uniqueIdCounter++}`; + + if (this._isSingleSelector) { + this.name = this.buttonToggleGroup.name; } - if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) { - this._checked = true; - } this._focusMonitor.monitor(this._elementRef.nativeElement, true); } @@ -384,31 +433,19 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase this._inputElement.nativeElement.focus(); } - /** 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. */ _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. - let groupValueChanged = this.buttonToggleGroup.selected != this; - this.checked = true; - this.buttonToggleGroup.selected = this; + this._checked = this._isSingleSelector ? true : !this._checked; + + if (this.buttonToggleGroup) { + this.buttonToggleGroup._syncButtonToggle(this, this._checked, true); this.buttonToggleGroup._onTouched(); - if (groupValueChanged) { - this.buttonToggleGroup._emitChangeEvent(); - } - } else { - this._toggle(); } // Emit a change event when the native input does. - this._emitChangeEvent(); + this.change.emit(new MatButtonToggleChange(this, this.value)); } _onInputClick(event: Event) { @@ -422,19 +459,6 @@ export class MatButtonToggle extends _MatButtonToggleMixinBase event.stopPropagation(); } - /** Dispatch change event with current value. */ - private _emitChangeEvent(): void { - let event = new MatButtonToggleChange(); - event.source = this; - event.value = this._value; - this.change.emit(event); - } - - // Unregister buttonToggleDispatcherListener on destroy - ngOnDestroy() { - this._removeUniqueSelectionListener(); - } - /** * Marks the button toggle as needing checking for change detection. * This method is exposed because the parent button toggle group will directly