ipsos-components
Version:
Material Design components for Angular
492 lines (415 loc) • 15.7 kB
text/typescript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {FocusMonitor} from '@angular/cdk/a11y';
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {UniqueSelectionDispatcher} from '@angular/cdk/collections';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChildren,
Directive,
ElementRef,
EventEmitter,
forwardRef,
Input,
OnDestroy,
OnInit,
Optional,
Output,
QueryList,
ViewChild,
ViewEncapsulation,
} from '@angular/core';
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from '@angular/forms';
import {CanDisable, mixinDisabled} from '@angular/material/core';
/** Acceptable types for a button toggle. */
export type ToggleType = 'checkbox' | 'radio';
// Boilerplate for applying mixins to MatButtonToggleGroup and MatButtonToggleGroupMultiple
/** @docs-private */
export class MatButtonToggleGroupBase {}
export const _MatButtonToggleGroupMixinBase = mixinDisabled(MatButtonToggleGroupBase);
/**
* Provider Expression that allows mat-button-toggle-group to register as a ControlValueAccessor.
* This allows it to support [(ngModel)].
* @docs-private
*/
export const MAT_BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => MatButtonToggleGroup),
multi: true
};
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;
}
/** Exclusive selection button toggle group that behaves like a radio-button group. */
export class MatButtonToggleGroup extends _MatButtonToggleGroupMixinBase
implements ControlValueAccessor, CanDisable {
/** 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 = `mat-button-toggle-group-${_uniqueIdCounter++}`;
/** Whether the button toggle group should be vertical. */
private _vertical: boolean = false;
/** The currently selected button toggle, should match the value. */
private _selected: MatButtonToggle | null = null;
/**
* The method to be called in order to update ngModel.
* Now `ngModel` binding is not supported in multiple selection mode.
*/
_controlValueAccessorChangeFn: (value: any) => void = () => {};
/** onTouch function registered via registerOnTouch (ControlValueAccessor). */
_onTouched: () => any = () => {};
/** Child button toggle buttons. */
_buttonToggles: QueryList<MatButtonToggle>;
/** `name` attribute for the underlying `input` element. */
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
this._updateButtonToggleNames();
}
/** Whether the toggle group is vertical. */
get vertical(): boolean {
return this._vertical;
}
set vertical(value) {
this._vertical = coerceBooleanProperty(value);
}
/** Value of the toggle group. */
get value(): any {
return this._value;
}
set value(newValue: any) {
if (this._value != newValue) {
this._value = newValue;
this.valueChange.emit(newValue);
this._updateSelectedButtonToggleFromValue();
}
}
/**
* Event that emits whenever the value of the group changes.
* Used to facilitate two-way data binding.
* @docs-private
*/
valueChange = new EventEmitter<any>();
/** Whether the toggle group is selected. */
get selected() {
return this._selected;
}
set selected(selected: MatButtonToggle | null) {
this._selected = selected;
this.value = selected ? selected.value : null;
if (selected && !selected.checked) {
selected.checked = true;
}
}
/** Event emitted when the group's value changes. */
change: EventEmitter<MatButtonToggleChange> = new EventEmitter<MatButtonToggleChange>();
constructor(private _changeDetector: ChangeDetectorRef) {
super();
}
private _updateButtonToggleNames(): void {
if (this._buttonToggles) {
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. */
_emitChangeEvent(): void {
let event = new MatButtonToggleChange();
event.source = this._selected;
event.value = this._value;
this._controlValueAccessorChangeFn(event.value);
this.change.emit(event);
}
/**
* 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();
}
/**
* Registers a callback that will be triggered when the value has changed.
* Implemented as part of ControlValueAccessor.
* @param fn On change callback function.
*/
registerOnChange(fn: (value: any) => void) {
this._controlValueAccessorChangeFn = fn;
}
/**
* Registers a callback that will be triggered when the control has been touched.
* Implemented as part of ControlValueAccessor.
* @param fn On touch callback function.
*/
registerOnTouched(fn: any) {
this._onTouched = fn;
}
/**
* Toggles the disabled state of the component. Implemented as part of ControlValueAccessor.
* @param isDisabled Whether the component should be disabled.
*/
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this._markButtonTogglesForCheck();
}
private _markButtonTogglesForCheck() {
if (this._buttonToggles) {
this._buttonToggles.forEach((toggle) => toggle._markForCheck());
}
}
}
/** Multiple selection button-toggle group. `ngModel` is not supported in this mode. */
export class MatButtonToggleGroupMultiple extends _MatButtonToggleGroupMixinBase
implements CanDisable {
/** Whether the button toggle group should be vertical. */
private _vertical: boolean = false;
/** Whether the toggle group is vertical. */
get vertical(): boolean {
return this._vertical;
}
set vertical(value) {
this._vertical = coerceBooleanProperty(value);
}
}
/** Single button inside of a toggle group. */
export class MatButtonToggle implements OnInit, OnDestroy {
/**
* Attached to the aria-label attribute of the host element. In most cases, arial-labelledby will
* take precedence so this may be omitted.
*/
ariaLabel: string = '';
/**
* Users can specify the `aria-labelledby` attribute which will be forwarded to the input element
*/
ariaLabelledby: string | null = null;
/** Whether or not this button toggle is checked. */
private _checked: boolean = false;
/** Type of the button toggle. Either 'radio' or 'checkbox'. */
_type: ToggleType;
/** Whether or not this button toggle is disabled. */
private _disabled: boolean = false;
/** Value assigned to this button toggle. */
private _value: any = null;
/** Whether or not the button toggle is a single selection. */
private _isSingleSelector: boolean = false;
/** Unregister function for _buttonToggleDispatcher **/
private _removeUniqueSelectionListener: () => void = () => {};
_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`;
}
/** The unique ID for this button toggle. */
id: string;
/** HTML's 'name' attribute used to group radios for unique selection. */
name: string;
/** Whether the button is checked. */
get checked(): boolean { return this._checked; }
set checked(newCheckedState: boolean) {
if (this._isSingleSelector && newCheckedState) {
// Notify all button toggles with the same name (in the same group) to un-check.
this._buttonToggleDispatcher.notify(this.id, this.name);
this._changeDetectorRef.markForCheck();
}
this._checked = newCheckedState;
if (newCheckedState && this._isSingleSelector && this.buttonToggleGroup.value != this.value) {
this.buttonToggleGroup.selected = this;
}
}
/** MatButtonToggleGroup reads this to assign its own value. */
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;
}
}
/** Whether the button is disabled. */
get disabled(): boolean {
return this._disabled || (this.buttonToggleGroup != null && this.buttonToggleGroup.disabled) ||
(this.buttonToggleGroupMultiple != null && this.buttonToggleGroupMultiple.disabled);
}
set disabled(value: boolean) {
this._disabled = coerceBooleanProperty(value);
}
/** Event emitted when the group value changes. */
change: EventEmitter<MatButtonToggleChange> = new EventEmitter<MatButtonToggleChange>();
constructor( toggleGroup: MatButtonToggleGroup,
toggleGroupMultiple: MatButtonToggleGroupMultiple,
private _changeDetectorRef: ChangeDetectorRef,
private _buttonToggleDispatcher: UniqueSelectionDispatcher,
private _elementRef: ElementRef,
private _focusMonitor: FocusMonitor) {
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++}`;
}
if (this.buttonToggleGroup && this._value == this.buttonToggleGroup.value) {
this._checked = true;
}
this._focusMonitor.monitor(this._elementRef.nativeElement, true);
}
/** Focuses the button. */
focus() {
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.buttonToggleGroup._onTouched();
if (groupValueChanged) {
this.buttonToggleGroup._emitChangeEvent();
}
} else {
this._toggle();
}
// Emit a change event when the native input does.
this._emitChangeEvent();
}
_onInputClick(event: Event) {
// We have to stop propagation for click events on the visual hidden input element.
// By default, when a user clicks on a label element, a generated click event will be
// dispatched on the associated input element. Since we are using a label element as our
// root container, the click event on the `slide-toggle` will be executed twice.
// The real click event will bubble up, and the generated click event also tries to bubble up.
// This will lead to multiple click events.
// Preventing bubbling for the second event will solve that issue.
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(): void {
this._removeUniqueSelectionListener();
}
/**
* Marks the button toggle as needing checking for change detection.
* This method is exposed because the parent button toggle group will directly
* update bound properties of the radio button.
*/
_markForCheck() {
// When group value changes, the button will not be notified. Use `markForCheck` to explicit
// update button toggle's status
this._changeDetectorRef.markForCheck();
}
}