feat(button-toggle): general component cleanup and support value input in multiple mode (#9191)

* Reworks the `MatButtonToggleGroup` component to remove the need for the `MatButtonToggleGroupMultiple` component and to avoid having to implement features in two places.
* Reworks the `MatButtonToggleGroup` to use the `SelectionModel` for managing its state.
* Switches all of the `async` button toggle tests to run in `fakeAsync`.

As a side-effect of the above-mentioned changes, the toggle group in multiple mode now supports the same inputs as the one in single-selection mode (`value` input/output, `name` input, `change` event etc.).

Note: this is along the same lines as #2773, however #2773 got left behind for too long and it would need a lot more work to fit in our current setup.

Fixes #9058.
This commit is contained in:
Kristiyan Kostadinov
2018-03-13 18:31:19 -04:00
committed by mmalerba
parent 2b745c4424
commit a403bacd81
3 changed files with 258 additions and 221 deletions
+3 -10
View File
@@ -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 {}
+50 -30
View File
@@ -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>(
MatButtonToggleGroupMultiple);
groupInstance = groupDebugElement.injector.get<MatButtonToggleGroup>(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(() => {
+205 -181
View File
@@ -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<MatButtonToggle>;
/**
* 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<any>();
/** 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<MatButtonToggleChange> =
@@ -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<MatButtonToggle>(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<MatButtonToggleChange>();
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