import { FocusMonitor } from '@angular/cdk/a11y' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { SelectionModel } from '@angular/cdk/collections' ; import { AfterContentInit , 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' ; let _uniqueIdCounter = 0 ; export const BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => ButtonToggleGroupDirective ) , multi : true , } ; export class ButtonToggleChange { constructor ( public source , public value ) { } } @ Directive ( { selector : 'gd-button-toggle-group' , providers : [ BUTTON_TOGGLE_GROUP_VALUE_ACCESSOR , ] , host : { 'class' : 'ButtonToggleGroup' , '[class.ButtonToggleGroup--vertical]' : '_vertical' , 'role' : 'group' , } , } ) export class ButtonToggleGroupDirective implements ControlValueAccessor , OnInit , AfterContentInit { _controlValueAccessorChangeFn : ( value : any ) => void = ( ) => { } ; /** onTouch function registered via registerOnTouch (ControlValueAccessor). */ _onTouched : ( ) => any = ( ) => { } ; /** Child button toggle buttons. */ @ ContentChildren ( forwardRef ( ( ) => ButtonToggleComponent ) ) _buttonToggles : QueryList < ButtonToggleComponent > ; @ Input ( ) disabled : boolean ; @ Output ( ) readonly valueChange = new EventEmitter < any > ( ) ; /** Event emitted when the group's value changes. */ @ Output ( ) readonly change : EventEmitter < ButtonToggleChange > = new EventEmitter < ButtonToggleChange > ( ) ; private _selectionModel : SelectionModel < ButtonToggleComponent > ; private _rawValue : any ; constructor ( private _changeDetector ) { } private _vertical = false ; /** Whether the toggle group is vertical. */ @ Input ( ) get vertical ( ) : boolean { return this . _vertical ; } set vertical ( value ) { this . _vertical = coerceBooleanProperty ( value ) ; } private _multiple = false ; /** Whether multiple button toggles can be selected. */ @ Input ( ) get multiple ( ) : boolean { return this . _multiple ; } set multiple ( value ) { this . _multiple = coerceBooleanProperty ( value ) ; } private _name = `gd-button-toggle-group-${ _uniqueIdCounter ++ }` ; /** `name` attribute for the underlying `input` element. */ @ Input ( ) get name ( ) : string { return this . _name ; } set name ( value ) { this . _name = value ; if ( this . _buttonToggles ) { this . _buttonToggles . forEach ( toggle => toggle . name = this . _name ) ; } } /** Value of the toggle group. */ @ Input ( ) 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 ) { this . _setSelectionByValue ( newValue ) ; this . valueChange . emit ( this . value ) ; } /** Selected button toggles in the group. */ get selected ( ) { const selected = this . _selectionModel . selected ; return this . multiple ? selected : ( selected [ 0 ] || null ) ; } ngOnInit ( ) { this . _selectionModel = new SelectionModel < ButtonToggleComponent > ( this . multiple , undefined , false ) ; } ngAfterContentInit ( ) { this . _selectionModel . select ( ... this . _buttonToggles . filter ( toggle => toggle . checked ) ) ; } param value writeValue ( value ) { this . value = value ; this . _changeDetector . markForCheck ( ) ; } registerOnChange ( fn ) { this . _controlValueAccessorChangeFn = fn ; } registerOnTouched ( fn ) { this . _onTouched = fn ; } setDisabledState ( isDisabled ) { this . disabled = isDisabled ; if ( this . _buttonToggles ) { this . _buttonToggles . forEach ( toggle => toggle . _markForCheck ( ) ) ; } } /** Dispatch change event with current selection and group value. */ _emitChangeEvent ( ) { const selected = this . selected ; const source = Array . isArray ( selected ) ? selected [ selected . length - 1 ] : selected ; const event = new ButtonToggleChange ( source , this . value ) ; this . _controlValueAccessorChangeFn ( event . value ) ; this . change . emit ( event ) ; } param toggle param select param isUserInput _syncButtonToggle ( toggle , select , isUserInput = false ) { if ( ! this . multiple && this . selected && ! toggle . checked ) { ( this . selected as ButtonToggleComponent ) . checked = false ; } if ( select ) { this . _selectionModel . select ( toggle ) ; } else { this . _selectionModel . deselect ( toggle ) ; } if ( isUserInput ) { this . _emitChangeEvent ( ) ; } this . valueChange . emit ( this . value ) ; } /** Checks whether a button toggle is selected. */ _isSelected ( toggle ) { return this . _selectionModel . isSelected ( toggle ) ; } /** Determines whether a button toggle should be checked on init. */ _isPrechecked ( toggle ) { if ( typeof this . _rawValue === 'undefined' ) { return false ; } if ( this . multiple && Array . isArray ( this . _rawValue ) ) { return this . _rawValue . some ( value => toggle . value != null && value === toggle . value ) ; } return toggle . value === this . _rawValue ; } /** Updates the selection state of the toggles in the group based on a value. */ private _setSelectionByValue ( value ) { this . _rawValue = value ; if ( ! this . _buttonToggles ) { 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 ) => 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 ) { const correspondingOption = this . _buttonToggles . find ( toggle => { return toggle . value != null && toggle . value === value ; } ) ; if ( correspondingOption ) { correspondingOption . checked = true ; this . _selectionModel . select ( correspondingOption ) ; } } } @ Component ( { selector : 'gd-button-toggle' , templateUrl : './button-toggle.html' , styleUrls : [ './button-toggle.scss' ] , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , host : { 'class' : 'ButtonToggle' , '[class.ButtonToggle--standalone]' : '!buttonToggleGroup' , '[class.ButtonToggle--checked]' : 'checked' , '[class.ButtonToggle--disabled]' : 'disabled' , '[attr.id]' : 'id' , } , } ) export class ButtonToggleComponent implements OnInit , OnDestroy { @ Input ( ) id : string ; @ Input ( ) value : any ; @ Input ( ) name : string ; @ Input ( ) ariaLabel : string ; @ Input ( ) ariaLabelledby : string | null = null ; @ ViewChild ( 'button' ) _buttonEl : ElementRef < HTMLButtonElement > ; buttonToggleGroup : ButtonToggleGroupDirective ; /** Event emitted when the group value changes. */ @ Output ( ) readonly change : EventEmitter < ButtonToggleChange > = new EventEmitter < ButtonToggleChange > ( ) ; private _isSingleSelector = false ; constructor ( @ Optional ( ) toggleGroup , private _changeDetectorRef , private _elementRef , private _focusMonitor ) { this . buttonToggleGroup = toggleGroup ; } private _checked = false ; @ Input ( ) get checked ( ) : boolean { return this . buttonToggleGroup ? this . buttonToggleGroup . _isSelected ( this ) : this . _checked ; } set checked ( value ) { const newValue = coerceBooleanProperty ( value ) ; if ( newValue !== this . _checked ) { this . _checked = newValue ; if ( this . buttonToggleGroup ) { this . buttonToggleGroup . _syncButtonToggle ( this , this . _checked ) ; } this . _changeDetectorRef . markForCheck ( ) ; } } get buttonId ( ) : string { return `${ this . id }-button` ; } private _disabled : boolean = false ; /** Whether the button is disabled. */ @ Input ( ) get disabled ( ) : boolean { return this . _disabled || ( this . buttonToggleGroup && this . buttonToggleGroup . disabled ) ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; } ngOnInit ( ) { this . _isSingleSelector = this . buttonToggleGroup && ! this . buttonToggleGroup . multiple ; this . id = this . id || `gd-button-toggle-${ _uniqueIdCounter ++ }` ; if ( this . _isSingleSelector ) { this . name = this . buttonToggleGroup . name ; } if ( this . buttonToggleGroup && this . buttonToggleGroup . _isPrechecked ( this ) ) { this . checked = true ; } this . _focusMonitor . monitor ( this . _elementRef . nativeElement , true ) ; } ngOnDestroy ( ) { this . _focusMonitor . stopMonitoring ( this . _elementRef . nativeElement ) ; } /** Focuses the button. */ focus ( ) { this . _buttonEl . nativeElement . focus ( ) ; } /** Checks the button toggle due to an interaction with the underlying native button. */ _onButtonClick ( ) { const newChecked = this . _isSingleSelector ? true : ! this . _checked ; if ( newChecked !== this . _checked ) { this . _checked = newChecked ; if ( this . buttonToggleGroup ) { this . buttonToggleGroup . _syncButtonToggle ( this , this . _checked , true ) ; this . buttonToggleGroup . _onTouched ( ) ; } } this . change . emit ( new ButtonToggleChange ( this , this . value ) ) ; } /**
     * 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 ( ) { this . _changeDetectorRef . markForCheck ( ) ; } }