From 5561cf3cf19e1783deb7c7dd013879c7d9c41c53 Mon Sep 17 00:00:00 2001 From: Ashley Hunter <20795331+ashley-hunter@users.noreply.github.com> Date: Tue, 26 Nov 2024 22:33:46 +0000 Subject: [PATCH 1/3] feat(radio-group): migrate to signals --- libs/ui/radio-group/brain/package.json | 3 +- libs/ui/radio-group/brain/src/index.ts | 4 +- .../src/lib/brn-radio-group.component.ts | 114 ++++ .../brain/src/lib/brn-radio-group.token.ts | 12 + .../brain/src/lib/brn-radio.component.ts | 537 ++++-------------- package.json | 1 - 6 files changed, 226 insertions(+), 445 deletions(-) create mode 100644 libs/ui/radio-group/brain/src/lib/brn-radio-group.component.ts create mode 100644 libs/ui/radio-group/brain/src/lib/brn-radio-group.token.ts diff --git a/libs/ui/radio-group/brain/package.json b/libs/ui/radio-group/brain/package.json index 287115d20..7ed15daab 100644 --- a/libs/ui/radio-group/brain/package.json +++ b/libs/ui/radio-group/brain/package.json @@ -4,7 +4,8 @@ "peerDependencies": { "@angular/core": ">=18.0.0", "@angular/cdk": ">=18.0.0", - "@angular/forms": ">=18.0.0" + "@angular/forms": ">=18.0.0", + "@spartan-ng/ui-forms-brain": "0.0.1-alpha.356" }, "dependencies": {}, "sideEffects": false, diff --git a/libs/ui/radio-group/brain/src/index.ts b/libs/ui/radio-group/brain/src/index.ts index c57460fcb..22f19056f 100644 --- a/libs/ui/radio-group/brain/src/index.ts +++ b/libs/ui/radio-group/brain/src/index.ts @@ -1,7 +1,9 @@ import { NgModule } from '@angular/core'; -import { BrnRadioComponent, BrnRadioGroupComponent } from './lib/brn-radio.component'; +import { BrnRadioGroupComponent } from './lib/brn-radio-group.component'; +import { BrnRadioComponent } from './lib/brn-radio.component'; +export * from './lib/brn-radio-group.component'; export * from './lib/brn-radio.component'; export const BrnRadioGroupImports = [BrnRadioGroupComponent, BrnRadioComponent] as const; diff --git a/libs/ui/radio-group/brain/src/lib/brn-radio-group.component.ts b/libs/ui/radio-group/brain/src/lib/brn-radio-group.component.ts new file mode 100644 index 000000000..fff367a19 --- /dev/null +++ b/libs/ui/radio-group/brain/src/lib/brn-radio-group.component.ts @@ -0,0 +1,114 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +import { BooleanInput } from '@angular/cdk/coercion'; +import { + booleanAttribute, + Component, + computed, + contentChildren, + forwardRef, + input, + model, + output, + signal, +} from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ChangeFn, TouchFn } from '@spartan-ng/ui-forms-brain'; +import { provideBrnRadioGroupToken } from './brn-radio-group.token'; +import { BrnRadioChange, BrnRadioComponent } from './brn-radio.component'; + +export const BRN_RADIO_GROUP_CONTROL_VALUE_ACCESSOR = { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => BrnRadioGroupComponent), + multi: true, +}; + +@Component({ + selector: 'brn-radio-group', + standalone: true, + providers: [BRN_RADIO_GROUP_CONTROL_VALUE_ACCESSOR, provideBrnRadioGroupToken(BrnRadioGroupComponent)], + host: { + role: 'radiogroup', + '(focusout)': 'onTouched()', + }, + template: '', +}) +export class BrnRadioGroupComponent implements ControlValueAccessor { + private static _nextUniqueId = 0; + + protected onChange: ChangeFn = () => {}; + + protected onTouched: TouchFn = () => {}; + + public readonly name = input(`brn-radio-group-${BrnRadioGroupComponent._nextUniqueId++}`); + + /** + * The value of the selected radio button. + */ + public readonly value = model(); + + /** + * Whether the radio group is disabled. + */ + public disabled = input(false, { + transform: booleanAttribute, + }); + + /** + * Whether the radio group should be required. + */ + public readonly required = input(false, { + transform: booleanAttribute, + }); + + /** + * The direction of the radio group. + */ + public readonly direction = input<'ltr' | 'rtl' | null>('ltr'); + + /** + * Event emitted when the group value changes. + */ + public readonly change = output>(); + + /** + * The internal disabled state of the radio group. This could be switched to a linkedSignal when we can drop v18 support. + * @internal + */ + public readonly disabledState = computed(() => signal(this.disabled())); + + /** + * Access the radio buttons within the group. + * @internal + */ + public readonly radioButtons = contentChildren(BrnRadioComponent, { descendants: true }); + + writeValue(value: T) { + this.value.set(value); + } + + registerOnChange(fn: ChangeFn) { + this.onChange = fn; + } + + registerOnTouched(fn: TouchFn) { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean) { + this.disabledState().set(isDisabled); + } + + /** + * Select a radio button. + * @internal + */ + select(radioButton: BrnRadioComponent, value: T) { + if (this.value() === value) { + return; + } + + this.value.set(value); + this.onChange(value); + this.change.emit(new BrnRadioChange(radioButton, value)); + } +} diff --git a/libs/ui/radio-group/brain/src/lib/brn-radio-group.token.ts b/libs/ui/radio-group/brain/src/lib/brn-radio-group.token.ts new file mode 100644 index 000000000..a19dce4bf --- /dev/null +++ b/libs/ui/radio-group/brain/src/lib/brn-radio-group.token.ts @@ -0,0 +1,12 @@ +import { ExistingProvider, inject, InjectionToken, Type } from '@angular/core'; +import type { BrnRadioGroupComponent } from './brn-radio-group.component'; + +const BrnRadioGroupToken = new InjectionToken>('BrnRadioGroupToken'); + +export function provideBrnRadioGroupToken(component: Type>): ExistingProvider { + return { provide: BrnRadioGroupToken, useExisting: component }; +} + +export function injectBrnRadioGroup(): BrnRadioGroupComponent { + return inject(BrnRadioGroupToken) as BrnRadioGroupComponent; +} diff --git a/libs/ui/radio-group/brain/src/lib/brn-radio.component.ts b/libs/ui/radio-group/brain/src/lib/brn-radio.component.ts index 428e9e5e8..1582d4db8 100644 --- a/libs/ui/radio-group/brain/src/lib/brn-radio.component.ts +++ b/libs/ui/radio-group/brain/src/lib/brn-radio.component.ts @@ -1,55 +1,38 @@ -import { FocusMonitor, type FocusOrigin, type FocusableOption } from '@angular/cdk/a11y'; -import { UniqueSelectionDispatcher } from '@angular/cdk/collections'; +import { FocusMonitor } from '@angular/cdk/a11y'; +import { BooleanInput } from '@angular/cdk/coercion'; import { - type AfterContentInit, - type AfterViewInit, ChangeDetectionStrategy, - ChangeDetectorRef, Component, - ContentChildren, - type DoCheck, ElementRef, - EventEmitter, - Input, type OnDestroy, - type OnInit, - Output, - type QueryList, - ViewChild, ViewEncapsulation, booleanAttribute, - forwardRef, + computed, inject, - numberAttribute, + input, + output, + viewChild, } from '@angular/core'; -import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { injectBrnRadioGroup } from './brn-radio-group.token'; -export const BRN_RADIO_GROUP_CONTROL_VALUE_ACCESSOR = { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => BrnRadioGroupComponent), - multi: true, -}; - -export class BrnRadioChange { +export class BrnRadioChange { constructor( - public source: BrnRadioComponent, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public value: any, + public source: BrnRadioComponent, + public value: T, ) {} } @Component({ selector: 'brn-radio', standalone: true, - imports: [], host: { class: 'brn-radio', - '[attr.id]': 'id', - '[class.brn-radio-checked]': 'checked', - '[class.brn-radio-disabled]': 'disabled', - '[attr.data-checked]': 'checked', - '[attr.data-disabled]': 'disabled', - '[attr.data-value]': 'value', + '[attr.id]': 'id()', + '[class.brn-radio-checked]': 'checked()', + '[class.brn-radio-disabled]': 'disabledState()', + '[attr.data-checked]': 'checked()', + '[attr.data-disabled]': 'disabledState()', + '[attr.data-value]': 'value()', // Needs to be removed since it causes some a11y issues (see #21266). '[attr.tabindex]': 'null', '[attr.aria-label]': 'null', @@ -58,215 +41,119 @@ export class BrnRadioChange { // Note: under normal conditions focus shouldn't land on this element, however it may be // programmatically set, for example inside of a focus trap, in this case we want to forward // the focus to the native element. - '(focus)': '_inputElement.nativeElement.focus()', + '(focus)': '_inputElement().nativeElement.focus()', }, exportAs: 'brnRadio', encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.OnPush, template: ` -
+
-