mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
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:
committed by
mmalerba
parent
2b745c4424
commit
a403bacd81
@@ -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 {}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user