import { FocusMonitor , FocusOrigin } from '@angular/cdk/a11y' ; import { coerceBooleanProperty } from '@angular/cdk/coercion' ; import { UniqueSelectionDispatcher } from '@angular/cdk/collections' ; import { AfterContentInit , AfterViewInit , ChangeDetectionStrategy , ChangeDetectorRef , Component , ContentChildren , Directive , ElementRef , EventEmitter , forwardRef , HostListener , Input , OnDestroy , OnInit , Optional , Output , QueryList , ViewChild , ViewEncapsulation , } from '@angular/core' ; import { ControlValueAccessor , NG_VALUE_ACCESSOR } from '@angular/forms' ; export class RadioChange { constructor ( public source , public value ) { } } export const RADIO_GROUP_CONTROL_VALUE_ACCESSOR = { provide : NG_VALUE_ACCESSOR , useExisting : forwardRef ( ( ) => RadioGroupDirective ) , multi : true , } ; let uniqueId = 0 ; @ Directive ( { selector : 'gd-radio-group' , providers : [ RADIO_GROUP_CONTROL_VALUE_ACCESSOR ] , host : { 'class' : 'RadioGroup' , 'role' : 'radiogroup' , } , } ) export class RadioGroupDirective implements AfterContentInit , ControlValueAccessor { _controlValueAccessorChangeFn : ( value : any ) => void = ( ) => { } ; onTouched : ( ) => any = ( ) => { } ; @ Output ( ) readonly change : EventEmitter < RadioChange > = new EventEmitter < RadioChange > ( ) ; @ ContentChildren ( forwardRef ( ( ) => RadioButtonComponent ) , { descendants : true } ) _radios : QueryList < RadioButtonComponent > ; /** Whether the `value` has been set to its initial value. */ private _isInitialized : boolean = false ; constructor ( private changeDetector ) { } /** Selected value for the radio group. */ private _value : any = null ; @ Input ( ) get value ( ) : any { return this . _value ; } set value ( newValue ) { if ( this . _value !== newValue ) { this . _value = newValue ; this . updateSelectedRadioFromValue ( ) ; this . _checkSelectedRadioButton ( ) ; } } /** The HTML name attribute applied to radio buttons in this group. */ private _name : string = `gd-radio-group-${ uniqueId ++ }` ; @ Input ( ) get name ( ) : string { return this . _name ; } set name ( value ) { this . _name = value ; this . updateRadioButtonNames ( ) ; } /** The currently selected radio button. Should match value. */ private _selected : RadioButtonComponent | null = null ; @ Input ( ) get selected ( ) { return this . _selected ; } set selected ( selected ) { this . _selected = selected ; this . value = selected ? selected . value : null ; this . _checkSelectedRadioButton ( ) ; } private _disabled : boolean ; /** Whether the radio group is disabled */ @ Input ( ) get disabled ( ) : boolean { return this . _disabled ; } set disabled ( value ) { this . _disabled = coerceBooleanProperty ( value ) ; this . _markRadiosForCheck ( ) ; } _checkSelectedRadioButton ( ) { if ( this . _selected && ! this . _selected . checked ) { this . _selected . checked = true ; } } ngAfterContentInit ( ) { this . _isInitialized = true ; } _touch ( ) { if ( this . onTouched ) { this . onTouched ( ) ; } } /** Dispatch change event with current selection and group value. */ _emitChangeEvent ( ) { if ( this . _isInitialized ) { this . change . emit ( new RadioChange ( this . _selected , this . _value ) ) ; } } param value writeValue ( value ) { this . value = value ; this . changeDetector . markForCheck ( ) ; } param fn registerOnChange ( fn ) { this . _controlValueAccessorChangeFn = fn ; } param fn registerOnTouched ( fn ) { this . onTouched = fn ; } param isDisabled setDisabledState ( isDisabled ) { this . disabled = isDisabled ; this . changeDetector . markForCheck ( ) ; } _markRadiosForCheck ( ) { if ( this . _radios ) { this . _radios . forEach ( radio => radio . _markForCheck ( ) ) ; } } private updateRadioButtonNames ( ) { if ( this . _radios ) { this . _radios . forEach ( radio => { radio . name = this . name ; } ) ; } } /** Updates the `selected` radio button from the internal _value state. */ private updateSelectedRadioFromValue ( ) { const isAlreadySelected = this . _selected !== null && this . _selected . value === this . _value ; if ( this . _radios && ! isAlreadySelected ) { this . _selected = null ; this . _radios . forEach ( radio => { radio . checked = this . value === radio . value ; if ( radio . checked ) { this . _selected = radio ; } } ) ; } } } @ Component ( { selector : 'gd-radio-button' , templateUrl : './radio.html' , styleUrls : [ './radio.scss' ] , changeDetection : ChangeDetectionStrategy . OnPush , encapsulation : ViewEncapsulation . None , host : { 'class' : 'RadioButton' , '[class.RadioButton--checked]' : 'checked' , '[attr.id]' : 'id' , } , } ) export class RadioButtonComponent implements AfterViewInit , OnInit , OnDestroy { @ Input ( ) name : string ; radioGroup : RadioGroupDirective ; @ Output ( ) readonly change : EventEmitter < RadioChange > = new EventEmitter < RadioChange > ( ) ; @ ViewChild ( 'input' ) _inputElement : ElementRef ; private removeUniqueSelectionListener : ( ) => void = ( ) => { } ; constructor ( @ Optional ( ) radioGroup , public _elementRef , private changeDetector , private focusMonitor , private radioDispatcher ) { this . radioGroup = radioGroup ; this . removeUniqueSelectionListener = radioDispatcher . listen ( ( id , name ) => { if ( id !== this . id && name === this . name ) { this . checked = false ; } } ) ; } private _id : string = `gd-radio-button-${ uniqueId ++ }` ; @ Input ( ) get id ( ) { return this . _id ; } set id ( value ) { this . _id = value ; } get inputId ( ) : string { return `${ this . _id }-input` ; } private _checked : boolean = false ; @ Input ( ) get checked ( ) : boolean { return this . _checked ; } set checked ( value ) { const newCheckedState = coerceBooleanProperty ( value ) ; if ( this . _checked !== newCheckedState ) { this . _checked = newCheckedState ; if ( newCheckedState && this . radioGroup && this . radioGroup . value !== this . value ) { this . radioGroup . selected = this ; } else if ( ! newCheckedState && this . radioGroup && this . radioGroup . value === this . value ) { this . radioGroup . selected = null ; } if ( newCheckedState ) { this . radioDispatcher . notify ( this . id , this . name ) ; } this . changeDetector . markForCheck ( ) ; } } private _value : any = null ; @ Input ( ) get value ( ) : any { return this . _value ; } set value ( value ) { if ( this . _value !== value ) { this . _value = value ; if ( this . radioGroup !== null ) { if ( ! this . checked ) { this . checked = this . radioGroup . value === value ; } if ( this . checked ) { this . radioGroup . selected = this ; } } } } ngOnInit ( ) { if ( this . radioGroup ) { this . checked = this . radioGroup . value === this . _value ; this . name = this . radioGroup . name ; } } ngAfterViewInit ( ) { this . focusMonitor . monitor ( this . _inputElement . nativeElement ) . subscribe ( focusOrigin => this . _onInputFocusChange ( focusOrigin ) ) ; } ngOnDestroy ( ) { this . focusMonitor . stopMonitoring ( this . _inputElement . nativeElement ) ; this . removeUniqueSelectionListener ( ) ; } _onInputClick ( event ) { event . stopPropagation ( ) ; } _onInputChange ( event ) { event . stopPropagation ( ) ; const groupValueChanged = this . radioGroup && this . value !== this . radioGroup . value ; this . checked = true ; this . _emitChangeEvent ( ) ; if ( this . radioGroup ) { this . radioGroup . _controlValueAccessorChangeFn ( this . value ) ; this . radioGroup . _touch ( ) ; if ( groupValueChanged ) { this . radioGroup . _emitChangeEvent ( ) ; } } } _markForCheck ( ) { this . changeDetector . markForCheck ( ) ; } private _emitChangeEvent ( ) { this . change . emit ( new RadioChange ( this , this . _value ) ) ; } @ HostListener ( 'focus' ) private onHostFocus ( ) { this . _inputElement . nativeElement . focus ( ) ; } private _onInputFocusChange ( origin ) { if ( origin ) { this . _elementRef . nativeElement . classList . add ( 'RadioButton--focused' ) ; } else { if ( this . radioGroup ) { this . radioGroup . _touch ( ) ; } this . _elementRef . nativeElement . classList . remove ( 'RadioButton--focused' ) ; } } }