/*
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
import {
AfterContentInit,
ChangeDetectionStrategy,
Component,
ContentChildren,
EventEmitter,
forwardRef,
Input,
OnDestroy,
Output,
QueryList,
PLATFORM_ID,
Inject,
ElementRef,
} from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
import { from, fromEvent, merge, Subject } from 'rxjs';
import { filter, startWith, switchMap, takeUntil } from 'rxjs/operators';
import { convertToBoolProperty, NbBooleanInput } from '../helpers';
import { NB_DOCUMENT } from '../../theme.options';
import { NbComponentOrCustomStatus } from '../component-status';
import { NbRadioComponent } from './radio.component';
/**
* The `NbRadioGroupComponent` is the wrapper for `nb-radio` button.
* It provides form bindings:
*
* ```html
*
* Option 1
* Option 2
* Option 3
*
* ```
*
* Also, you can use `value` and `valueChange` for binding without forms.
*
* ```html
*
* Option 1
* Option 2
* Option 3
*
* ```
*
* Radio items name has to be provided through `name` input property of the radio group.
*
* ```html
*
* ...
*
* ```
*
* You can change radio group status by setting `status` input.
* @stacked-example(Statuses, radio/radio-statuses.component)
*
* Also, you can disable the whole group using `disabled` attribute.
* @stacked-example(Disabled group, radio/radio-disabled-group.component)
*
* */
@Component({
selector: 'nb-radio-group',
template: `
`,
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NbRadioGroupComponent),
multi: true,
},
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class NbRadioGroupComponent implements AfterContentInit, OnDestroy, ControlValueAccessor {
protected destroy$ = new Subject();
protected onChange = (value: any) => {};
protected onTouched = () => {};
@Input()
get value(): any {
return this._value;
}
set value(value: any) {
this._value = value;
this.updateValues();
}
protected _value: any;
@Input()
get name(): string {
return this._name;
}
set name(name: string) {
this._name = name;
this.updateNames();
}
protected _name: string;
@Input()
get disabled(): boolean {
return this._disabled;
}
set disabled(disabled: boolean) {
this._disabled = convertToBoolProperty(disabled);
this.updateDisabled();
}
protected _disabled: boolean;
static ngAcceptInputType_disabled: NbBooleanInput;
/**
* Radio buttons status.
* Possible values are `primary` (default), `success`, `warning`, `danger`, `info`.
*/
@Input()
get status(): NbComponentOrCustomStatus {
return this._status;
}
set status(value: NbComponentOrCustomStatus) {
if (this._status !== value) {
this._status = value;
this.updateStatus();
}
}
protected _status: NbComponentOrCustomStatus = 'basic';
@ContentChildren(NbRadioComponent, { descendants: true }) radios: QueryList;
@Output() valueChange: EventEmitter = new EventEmitter();
constructor(
protected hostElement: ElementRef,
@Inject(PLATFORM_ID) protected platformId,
@Inject(NB_DOCUMENT) protected document,
) {}
ngAfterContentInit() {
// In case option 'name' isn't set on nb-radio component,
// we need to set it's name right away, so it won't overlap with options
// without names from other radio groups. Otherwise they all would have
// same name and will be considered as options from one group so only the
// last option will stay selected.
this.updateNames();
this.radios.changes
.pipe(
startWith(this.radios),
// 'changes' emit during change detection run and we can't update
// option properties right of since they already was initialized.
// Instead we schedule microtask to update radios after change detection
// run is finished and trigger one more change detection run.
switchMap((radios: QueryList) => from(Promise.resolve(radios))),
takeUntil(this.destroy$),
)
.subscribe(() => this.updateAndSubscribeToRadios());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
writeValue(value: any): void {
this.value = value;
}
setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
}
protected updateAndSubscribeToRadios() {
this.updateValueFromCheckedOption();
this.updateNames();
this.updateValues();
this.updateDisabled();
this.updateStatus();
this.subscribeOnRadiosValueChange();
this.subscribeOnRadiosBlur();
}
protected updateNames() {
if (this.radios) {
this.radios.forEach((radio: NbRadioComponent) => radio._setName(this.name));
}
}
protected updateValues() {
this.updateAndMarkForCheckRadios((radio: NbRadioComponent) => radio.checked = radio.value === this.value);
}
protected updateDisabled() {
if (typeof this.disabled !== 'undefined') {
this.updateAndMarkForCheckRadios((radio: NbRadioComponent) => radio.disabled = this.disabled);
}
}
protected subscribeOnRadiosValueChange() {
if (!this.radios || !this.radios.length) {
return;
}
merge(...this.radios.map((radio: NbRadioComponent) => radio.valueChange))
.pipe(
takeUntil(
merge(
this.radios.changes,
this.destroy$,
),
),
)
.subscribe((value: any) => {
this.writeValue(value);
this.propagateValue(value);
});
}
protected propagateValue(value: any) {
this.valueChange.emit(value);
this.onChange(value);
}
protected subscribeOnRadiosBlur() {
const hasNoRadios = !this.radios || !this.radios.length;
if (!isPlatformBrowser(this.platformId) || hasNoRadios) {
return;
}
const hostElement = this.hostElement.nativeElement;
fromEvent(hostElement, 'focusin')
.pipe(
filter(event => hostElement.contains(event.target as Node)),
switchMap(() => merge(
fromEvent(this.document, 'focusin'),
fromEvent(this.document, 'click'),
)),
filter(event => !hostElement.contains(event.target as Node)),
takeUntil(
merge(
this.radios.changes,
this.destroy$,
),
),
)
.subscribe(() => this.onTouched());
}
protected updateStatus() {
this.updateAndMarkForCheckRadios((radio: NbRadioComponent) => radio.status = this.status);
}
protected updateAndMarkForCheckRadios(updateFn: (NbRadioComponent) => void) {
if (this.radios) {
this.radios.forEach((radio) => {
updateFn(radio);
radio._markForCheck();
});
}
}
protected updateValueFromCheckedOption() {
const checkedRadio = this.radios.find((radio) => radio.checked);
const isValueMissing = this.value === undefined || this.value === null;
if (checkedRadio && isValueMissing && checkedRadio.value !== this.value) {
this.value = checkedRadio.value;
}
}
}