@libs-ui/components-radio-single
v0.2.357-11
Published
> Component hiển thị một radio button đơn lẻ, hỗ trợ label, avatar, hình ảnh, popover và custom styling linh hoạt.
Readme
@libs-ui/components-radio-single
Component hiển thị một radio button đơn lẻ, hỗ trợ label, avatar, hình ảnh, popover và custom styling linh hoạt.
Giới thiệu
LibsUiComponentsRadioSingleComponent là một standalone Angular component dùng để hiển thị một radio button đơn. Component hoạt động theo mô hình two-way binding qua model<boolean>, cho phép parent kiểm soát trạng thái active và xử lý group logic bên ngoài. Component hỗ trợ đa dạng tùy chỉnh giao diện bao gồm label có i18n, avatar, bullet, icon tùy chỉnh, popover tooltip, và component outlet động.
Tính năng
- ✅ Two-way binding — Trạng thái
activedùngmodel<boolean>, hỗ trợ cả controlled và uncontrolled pattern. - ✅ Rich content — Hỗ trợ label (có i18n interpolation), avatar, hình ảnh, bullet, popover tooltip.
- ✅ Click behavior linh hoạt —
clickExactlyđiều khiển vùng click: chỉ icon/label hoặc toàn bộ container. - ✅ Disable state — Hỗ trợ disable hoàn toàn hoặc chỉ disable label riêng biệt.
- ✅ Image fallback — Tự động dùng
linkImageErrorkhilinkImagebị lỗi tải. - ✅ Component outlet — Có thể inject component động vào bên trong radio qua
componentOutlet. - ✅ Custom styling — Cung cấp nhiều input class cho container, label, icon.
- ✅ OnPush + Signals — Hiệu năng cao với Angular change detection tối ưu.
Khi nào sử dụng
- Khi cần một radio button hoạt động độc lập mà không cần group logic phức tạp.
- Khi muốn tùy chỉnh layout radio button kết hợp với avatar, hình ảnh, hoặc popover.
- Khi xây dựng các component phức tạp hơn như Radio Group, Select List, hay option list custom.
- Khi cần radio button hỗ trợ i18n translate cho label.
Cài đặt
npm install @libs-ui/components-radio-singleImport
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
// ...
})
export class MyComponent {}Ví dụ sử dụng
1. Radio button cơ bản
// my.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
import { IRadioEvent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected isActive = signal(false);
protected handlerChange(event: IRadioEvent): void {
event; // IRadioEvent: { active: boolean; key: unknown }
this.isActive.set(event.active);
}
}<!-- my.component.html -->
<libs_ui-components-radio-single
[active]="isActive()"
(outChange)="handlerChange($event)"
[key]="'option-1'"
label="Tùy chọn 1"
/>2. Radio button với disabled state
// my.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected activeOption = signal(true);
protected inactiveOption = signal(false);
}<!-- my.component.html -->
<!-- Disabled và đang active -->
<libs_ui-components-radio-single
[active]="activeOption()"
[disable]="true"
label="Đã chọn (vô hiệu hóa)"
/>
<!-- Disabled và không active -->
<libs_ui-components-radio-single
[active]="inactiveOption()"
[disable]="true"
label="Chưa chọn (vô hiệu hóa)"
/>3. Điều khiển vùng click bằng clickExactly
// my.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
import { IRadioEvent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected clickAnywhereActive = signal(false);
protected clickExactActive = signal(false);
protected handlerClickAnywhere(event: IRadioEvent): void {
event.active;
this.clickAnywhereActive.set(event.active);
}
protected handlerClickExact(event: IRadioEvent): void {
event.active;
this.clickExactActive.set(event.active);
}
}<!-- my.component.html -->
<!-- clickExactly=false: click vào bất kỳ đâu trong container đều toggle -->
<libs_ui-components-radio-single
[active]="clickAnywhereActive()"
(outChange)="handlerClickAnywhere($event)"
[clickExactly]="false"
classInclude="flex items-center w-full"
label="Click vào container sẽ toggle"
/>
<!-- clickExactly=true (mặc định): chỉ click đúng icon hoặc label mới toggle -->
<libs_ui-components-radio-single
[active]="clickExactActive()"
(outChange)="handlerClickExact($event)"
[clickExactly]="true"
classInclude="flex items-center w-full"
label="Phải click đúng icon hoặc label"
/>4. Radio button với Avatar
// my.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
import { IRadioEvent } from '@libs-ui/components-radio-single';
import { IAvatarConfig } from '@libs-ui/components-avatar';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected userActive = signal(false);
protected readonly userAvatarConfig: IAvatarConfig = {
linkAvatar: 'https://example.com/user-avatar.jpg',
linkAvatarError: 'https://example.com/default-avatar.jpg',
size: 32,
typeShape: 'circle',
textAvatar: 'ND',
idGenColor: 'user-001',
};
protected handlerUserChange(event: IRadioEvent): void {
event.key; // 'user-001'
this.userActive.set(event.active);
}
}<!-- my.component.html -->
<libs_ui-components-radio-single
[active]="userActive()"
(outChange)="handlerUserChange($event)"
[key]="'user-001'"
[avatarConfig]="userAvatarConfig"
label="Nguyễn Dũng"
classLabelInclude="libs-ui-font-h4r ml-2"
/>5. Radio button với i18n interpolation
// my.component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
import { IRadioEvent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected planActive = signal(false);
protected readonly labelParams: Record<string, unknown> = {
price: '99,000',
currency: 'VND',
};
protected handlerPlanChange(event: IRadioEvent): void {
event.active;
this.planActive.set(event.active);
}
}<!-- my.component.html -->
<!-- label là i18n key, labelInterpolateParams truyền tham số vào translate pipe -->
<libs_ui-components-radio-single
[active]="planActive()"
(outChange)="handlerPlanChange($event)"
[key]="'plan-basic'"
label="i18n_plan_price_label"
[labelInterpolateParams]="labelParams"
/>6. Radio button trong group (pattern thường dùng)
// my.component.ts
import { Component, signal, computed } from '@angular/core';
import { LibsUiComponentsRadioSingleComponent } from '@libs-ui/components-radio-single';
import { IRadioEvent } from '@libs-ui/components-radio-single';
@Component({
standalone: true,
imports: [LibsUiComponentsRadioSingleComponent],
templateUrl: './my.component.html',
})
export class MyComponent {
protected selectedKey = signal<string>('option-a');
protected readonly options = [
{ key: 'option-a', label: 'Tùy chọn A' },
{ key: 'option-b', label: 'Tùy chọn B' },
{ key: 'option-c', label: 'Tùy chọn C' },
];
protected isActive = computed(() => (key: string) => this.selectedKey() === key);
protected handlerChange(event: IRadioEvent): void {
event.stopPropagation?.();
this.selectedKey.set(event.key as string);
}
}<!-- my.component.html -->
@for (option of options; track option.key) {
<libs_ui-components-radio-single
[active]="selectedKey() === option.key"
(outChange)="handlerChange($event)"
[key]="option.key"
[label]="option.label"
/>
}@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [active] | model<boolean> | false | Trạng thái active (checked) của radio. Hỗ trợ two-way binding. | [active]="isActive()" hoặc [(active)]="isActive" |
| [key] | unknown | undefined | Key định danh cho radio button, được emit kèm trong outChange. | [key]="'option-1'" |
| [label] | string | '' | Label hiển thị bên cạnh radio. Hỗ trợ i18n key (qua TranslateModule). | label="Tùy chọn 1" |
| [labelInterpolateParams] | Record<string, unknown> | {} | Tham số truyền vào translate pipe cho label i18n. | [labelInterpolateParams]="{ name: 'Admin' }" |
| [disable] | boolean | false | Vô hiệu hóa hoàn toàn radio, không cho phép click hay toggle. | [disable]="true" |
| [disableLabel] | boolean | false | Chỉ vô hiệu hóa visual của label (mờ), vẫn cho phép click icon. | [disableLabel]="true" |
| [clickExactly] | boolean | true | true: chỉ click đúng icon/label mới toggle. false: click vào container cũng toggle. | [clickExactly]="false" |
| [typeRadio] | 'normal' \| 'medium' | 'normal' | Kích thước/kiểu dáng của icon radio. | [typeRadio]="'medium'" |
| [ignoreRadio] | boolean | undefined | Ẩn icon radio, chỉ hiển thị label/avatar/image. | [ignoreRadio]="true" |
| [ignorePopoverLabel] | boolean | undefined | Không hiển thị popover của label dù đã cấu hình. | [ignorePopoverLabel]="true" |
| [linkImage] | string | '' | URL hình ảnh hiển thị cạnh radio. | [linkImage]="'https://example.com/img.png'" |
| [linkImageError] | string | '' | URL hình ảnh dự phòng khi linkImage tải thất bại. | [linkImageError]="'https://example.com/default.png'" |
| [imgTypeIcon] | boolean | undefined | Nếu true, hình ảnh được cố định kích thước 18x18px (dùng cho icon nhỏ). | [imgTypeIcon]="true" |
| [avatarConfig] | IAvatarConfig | undefined | Cấu hình hiển thị avatar bên cạnh radio. | [avatarConfig]="{ linkAvatar: '...', size: 32, typeShape: 'circle' }" |
| [bullet] | Record<string, string> | undefined | Cấu hình bullet point (chấm màu) cạnh radio. Key backgroundColor là màu nền. | [bullet]="{ backgroundColor: '#22c55e' }" |
| [popover] | IPopover | undefined | Cấu hình popover tooltip hiển thị kèm icon thông tin. | [popover]="{ config: { content: 'Mô tả thêm' } }" |
| [zIndexLabel] | number | 1200 | z-index của label (dùng khi label nằm trong modal/dropdown). | [zIndexLabel]="1300" |
| [classInclude] | string | '' | Custom CSS class thêm vào container ngoài cùng. | classInclude="flex items-center gap-2" |
| [classLabelInclude] | string | 'libs-ui-font-h4r ' | Custom CSS class cho label. Ghi đè default font class. | classLabelInclude="libs-ui-font-h5r text-gray-600" |
| [classIncludeIcon] | string | '' | Custom CSS class cho icon radio. | classIncludeIcon="text-blue-600" |
| [classImageInclude] | string | '' | Custom CSS class cho thẻ <img>. | classImageInclude="rounded-full" |
| [dataComponentOutlet] | TYPE_COMPONENT_OUTLET_DATA | undefined | Data truyền vào component được inject qua componentOutlet. | [dataComponentOutlet]="{ name: 'value' }" |
| [componentOutlet] | any | undefined | Component động được inject vào bên trong radio qua ngComponentOutlet. | [componentOutlet]="MyBadgeComponent" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outChange) | IRadioEvent | Emit khi trạng thái radio thay đổi (toggle active). Chỉ emit khi chưa active và không bị disable. | handlerChange(event: IRadioEvent): void { event.stopPropagation?.(); this.active.set(event.active); } | (outChange)="handlerChange($event)" |
| (outClickLabel) | void | Emit khi người dùng click vào label. | handlerClickLabel(): void { /* xử lý click label */ } | (outClickLabel)="handlerClickLabel()" |
| (outChangStageFlagMousePopover) | IFlagMouse | Emit trạng thái chuột khi hover/leave popover (dùng để đồng bộ trạng thái popover với parent). | handlerFlagMouse(flag: IFlagMouse): void { flag.stopPropagation?.(); this.flagMouse.set(flag); } | (outChangStageFlagMousePopover)="handlerFlagMouse($event)" |
Types & Interfaces
import { IRadioEvent, IRadioItem } from '@libs-ui/components-radio-single';
import { IAvatarConfig } from '@libs-ui/components-avatar';
import { IPopover } from '@libs-ui/components-popover';// Event emit khi radio thay đổi trạng thái
export interface IRadioEvent {
active: boolean; // Trạng thái active mới sau khi thay đổi
key: any; // Key của radio button (từ input [key])
item?: any; // Data item đi kèm (tùy chọn, dùng khi integrate với list)
}
// Cấu hình cho từng item trong radio group (dùng khi xây Radio Group)
export interface IRadioItem {
key: any; // Key định danh bắt buộc
active: boolean; // Trạng thái active
classInclude?: string; // Custom class container
label?: string; // Label hiển thị (hỗ trợ i18n key)
labelInterpolateParams?: Record<string, unknown>; // Params cho translate
ignorePopoverLabel?: boolean; // Ẩn popover của label
classLabelInclude?: string; // Custom class label
popover?: IPopover; // Cấu hình popover
disable?: boolean; // Vô hiệu hóa
disableLabel?: boolean; // Vô hiệu hóa label
clickExactly?: boolean; // Điều khiển vùng click
zIndexLabel?: number; // z-index label
avatarConfig?: IAvatarConfig; // Cấu hình avatar
data?: any; // Data tùy ý đính kèm
[key: string]: any; // Mở rộng thêm fields
}Lưu ý quan trọng
⚠️ Logic nhóm radio phải xử lý ở parent: Component này là một radio button đơn lẻ, không tự tắt các radio khác. Parent component phải tự xử lý logic "chỉ một radio được active" bằng cách theo dõi selectedKey và truyền [active]="selectedKey() === item.key" cho từng radio.
⚠️ clickExactly mặc định là true: Mặc định người dùng phải click chính xác vào icon radio hoặc text label mới trigger toggle. Để click vào vùng container cũng trigger, cần truyền [clickExactly]="false" vào component.
⚠️ Radio không tự toggle lại về false: Một khi radio đã active = true, handlerClick và changeActive sẽ không làm gì (guard if (this.active()) trả về sớm). Đây là hành vi chuẩn của radio button — không cho phép bỏ chọn khi đã chọn. Nếu muốn toggle, cần xử lý bên ngoài.
⚠️ classLabelInclude ghi đè hoàn toàn: Khi truyền classLabelInclude, giá trị mặc định 'libs-ui-font-h4r ' sẽ bị thay thế hoàn toàn. Nếu muốn giữ typography mặc định và thêm class khác, cần include lại: classLabelInclude="libs-ui-font-h4r text-gray-600".
⚠️ Image fallback dùng AfterViewInit: Cơ chế fallback cho linkImageError chỉ hoạt động sau khi view khởi tạo. Nếu linkImage thay đổi dynamically, linkImageDisplay signal sẽ reset về linkImage mới nhờ effect(), nhưng listener lỗi chỉ được đăng ký một lần trong ngAfterViewInit.
