Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions docs/components-api/radiobutton.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,22 @@

# ExtraRadioButton

| Свойство | Описание | Типизация |
| ---------------- | --------------------------------- | ------------------ |
| `label` | текст названия | `string` |
| `label-position` | положение лейбла | `default \| left` |
| `caption` | текст пояснения под лейблом | `string` |
| `value` | значение опции | `any` |
> ✅ **Реализован**: `ExtraRadiobuttonComponent` (`@cdek-it/angular-ui-kit`) соответствует спецификации.

| Свойство | Описание | Типизация | По умолчанию |
| ---------------- | --------------------------------- | ------------------ | ------------ |
| `label` | текст названия | `string` | `''` |
| `label-position` | положение лейбла | `right \| left` | `right` |
| `caption` | текст пояснения под лейблом | `string` | `''` |
| `value` | значение опции | `any` | `null` |

# События

| Событие | Описание | Типизация |
| ---------- | ----------------------------- | ---------------------------------------------- |
| `onClick` | срабатывает при выборе опции | `(event: ExtraRadioButtonClickEvent) => void` |
| `onFocus` | срабатывает при получении фокуса | `(event: Event) => void` |
| `onBlur` | срабатывает при потере фокуса | `(event: Event) => void` |

# ExtraRadioButtonClickEvent

Expand Down
149 changes: 104 additions & 45 deletions src/lib/components/radiobutton/radiobutton.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,36 @@
import { ChangeDetectionStrategy, Component, EventEmitter, forwardRef, Input, Output } from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
forwardRef,
inject,
Injector,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { ControlValueAccessor, FormsModule, NG_VALUE_ACCESSOR, NgControl } from '@angular/forms';
import { NgTemplateOutlet } from '@angular/common';
import { RadioButton, RadioButtonClickEvent } from 'primeng/radiobutton';
import { Subscription } from 'rxjs';

export type ExtraRadiobuttonVariant = 'outlined' | 'filled';
export type ExtraRadiobuttonSize = 'small' | 'base' | 'large';
export type ExtraRadioButtonClickEvent = RadioButtonClickEvent;
export type ExtraRadiobuttonLabelPosition = 'right' | 'left';

export interface ExtraRadioButtonClickEvent {
value: any;
originalEvent: Event;
}

let nextInputId = 0;

@Component({
selector: 'extra-radiobutton',
standalone: true,
imports: [RadioButton, FormsModule, NgTemplateOutlet],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [RadioButton, FormsModule],
host: { style: 'display: contents' },
providers: [
{
provide: NG_VALUE_ACCESSOR,
Expand All @@ -19,65 +39,103 @@ export type ExtraRadioButtonClickEvent = RadioButtonClickEvent;
}
],
template: `
<p-radiobutton
[value]="value"
[name]="name ?? ''"
[disabled]="disabled"
[invalid]="invalid"
[(ngModel)]="modelValue"
[variant]="primeVariant"
[size]="primeSize"
[inputId]="inputId"
[tabindex]="tabindex"
[ariaLabel]="ariaLabel"
[ariaLabelledBy]="ariaLabelledBy"
[autofocus]="autofocus"
(onClick)="onClickHandler($event)"
(onFocus)="onFocus.emit($event)"
(onBlur)="onBlur.emit($event)"
></p-radiobutton>
@if (label || caption) {
<div class="extra-radiobutton" [class.extra-radiobutton--left]="labelPosition === 'left'">
<ng-container [ngTemplateOutlet]="fieldTpl" />
<div class="extra-radiobutton-body">
@if (label) {
<label class="radio-label" [class.radio-label--disabled]="disabled" [for]="inputId">{{ label }}</label>
}
@if (caption) {
<div class="radio-caption" [class.radio-caption--disabled]="disabled">{{ caption }}</div>
}
</div>
</div>
} @else {
<ng-container [ngTemplateOutlet]="fieldTpl" />
}

<ng-template #fieldTpl>
<p-radiobutton
[value]="value"
[name]="name ?? ''"
[(ngModel)]="modelValue"
[disabled]="disabled"
[invalid]="invalid"
[inputId]="inputId"
(onClick)="onClickHandler($event)"
(onFocus)="onFocus.emit($event)"
(onBlur)="onBlur.emit($event)"
></p-radiobutton>
</ng-template>
`
})
export class ExtraRadiobuttonComponent implements ControlValueAccessor {
export class ExtraRadiobuttonComponent implements ControlValueAccessor, OnInit, OnDestroy {
private readonly _injector = inject(Injector);
private readonly _cdr = inject(ChangeDetectorRef);
private _ngControl: NgControl | null = null;
private _statusSub?: Subscription;
private _valueSub?: Subscription;

ngOnInit(): void {
this._ngControl = this._injector.get(NgControl, null, { self: true, optional: true });
/**
* invalid — геттер поверх NgControl.invalid; на OnPush не пересчитывается сам по себе,
* когда валидность меняется извне (Validators/updateValueAndValidity без локального события).
*/
this._statusSub = this._ngControl?.statusChanges?.subscribe(() => this._cdr.markForCheck());
/**
* Несколько radiobutton могут сидеть на ОДНОМ общем FormControl (радио-группа). Angular Forms
* при смене значения "изнутри" (клик по одной из них) вызывает control.setValue(value,
* { emitModelToViewChange: false }) — этот флаг намеренно НЕ вызывает writeValue() у соседних
* директив, привязанных к тому же control (см. updateControl() в @angular/forms). Из-за этого
* соседние radiobutton в группе никогда не узнавали, что должны снять выбор. valueChanges
* эмитится всегда, независимо от этого флага — используем его для синхронизации между соседями.
*/
this._valueSub = this._ngControl?.valueChanges?.subscribe((value) => {
this.modelValue = value;
this._cdr.markForCheck();
});
}

ngOnDestroy(): void {
this._statusSub?.unsubscribe();
this._valueSub?.unsubscribe();
}

@Input() label = '';
@Input() labelPosition: ExtraRadiobuttonLabelPosition = 'right';
@Input() caption = '';
@Input() value: any = null;
/** Имя группы; форм-обвязка, вне спеки — нужна для нативной семантики радио-группы. */
@Input() name: string | undefined = undefined;
@Input() disabled = false;
@Input() invalid = false;
@Input() variant: ExtraRadiobuttonVariant = 'outlined';
@Input() size: ExtraRadiobuttonSize = 'base';
@Input() inputId: string | undefined = undefined;
@Input() tabindex: number | undefined = undefined;
@Input() ariaLabel: string | undefined = undefined;
@Input() ariaLabelledBy: string | undefined = undefined;
@Input() autofocus = false;

@Output() onClick = new EventEmitter<ExtraRadioButtonClickEvent>();
@Output() onFocus = new EventEmitter<Event>();
@Output() onBlur = new EventEmitter<Event>();

modelValue: any = null;
/** Уникальный id поля для связи label ↔ input. */
readonly inputId = `extra-radiobutton-${nextInputId++}`;

private _onChange: (value: any) => void = () => {};
private _onTouched: () => void = () => {};
disabled = false;
modelValue: any = null;

get primeSize(): 'small' | 'large' | undefined {
if (this.size === 'small') return 'small';
if (this.size === 'large') return 'large';
return undefined;
get invalid(): boolean {
return this._ngControl?.invalid ?? false;
}

get primeVariant(): 'filled' | undefined {
return this.variant === 'filled' ? 'filled' : undefined;
}
private _onChange: (value: any) => void = () => {};
private _onTouched: () => void = () => {};

onClickHandler(event: ExtraRadioButtonClickEvent): void {
onClickHandler(event: RadioButtonClickEvent): void {
this._onChange(event.value);
this._onTouched();
this.onClick.emit(event);
this.onClick.emit({ value: event.value, originalEvent: event.originalEvent as Event });
}

writeValue(value: any): void {
this.modelValue = value;
this._cdr.markForCheck();
}

registerOnChange(fn: (value: any) => void): void {
Expand All @@ -90,5 +148,6 @@ export class ExtraRadiobuttonComponent implements ControlValueAccessor {

setDisabledState(isDisabled: boolean): void {
this.disabled = isDisabled;
this._cdr.markForCheck();
}
}
Loading
Loading