@libs-ui/components-modal
v0.2.357-4
Published
> Component Modal linh hoạt hỗ trợ 2 phiên bản: Modal chuẩn (content projection) và Modal V2 (Dynamic Component + Event-Driven bidirectional stream).
Readme
@libs-ui/components-modal
Component Modal linh hoạt hỗ trợ 2 phiên bản: Modal chuẩn (content projection) và Modal V2 (Dynamic Component + Event-Driven bidirectional stream).
Giới thiệu
@libs-ui/components-modal cung cấp hai component modal chính phục vụ các nhu cầu khác nhau trong ứng dụng Angular. LibsUiComponentsModalComponent (phiên bản chuẩn) cho phép nhúng nội dung trực tiếp qua content projection trong template HTML. LibsUiComponentsModalV2Component (phiên bản V2) sử dụng kiến trúc Event-Driven hiện đại với Dynamic Component Injection và Subject stream để giao tiếp 2 chiều giữa modal cha và component con — phù hợp cho các tình huống phức tạp cần type-safety cao và lazy loading thực sự.
Tính năng
- ✅ Hai chế độ hiển thị:
center(hộp thoại giữa màn hình) vàoffset-right(panel trượt từ bên phải) - ✅ Modal V2: tải Component động vào Header, Body, Footer qua
getComponentOutlet()(lazy load thực sự) - ✅ Modal V2: giao tiếp 2 chiều type-safe qua RxJS Subject (
eventToComponent/eventToModal) - ✅ Footer 3 vùng (left / center / right) với button loading management theo
key - ✅ Tích hợp Skeleton loading cho từng section (Header, Body, Footer)
- ✅ Hỗ trợ đóng modal qua phím
Escape, click outside, hoặc nút X - ✅ Chế độ fullscreen tự động set width/height = 100vw/100vh
- ✅ Hỗ trợ micro-frontend: tự động gửi
PostMessagetới parent window khi mở/đóng - ✅ Angular Signals +
ChangeDetectionStrategy.OnPush— tối ưu hiệu năng
Khi nào sử dụng
Dùng LibsUiComponentsModalComponent (Modal chuẩn) khi:
- Cần xác nhận một hành động quan trọng từ người dùng (confirm dialog)
- Hiển thị form nhập liệu hoặc thông tin chi tiết dạng side panel đơn giản
- Thông báo trạng thái (thành công / lỗi / cảnh báo) với nội dung cố định
- Nội dung modal có thể được khai báo trực tiếp trong template HTML
Dùng LibsUiComponentsModalV2Component (Modal V2) khi:
- Muốn mở modal từ TypeScript mà không cần khai báo tag trong template HTML
- Cần lazy-load component con cho Body/Header/Footer (tối ưu bundle size)
- Yêu cầu giao tiếp sự kiện phức tạp 2 chiều giữa modal cha và component con
- Cần type-safety cao cho các tham số sự kiện (không dùng
any) - Cần quản lý trạng thái loading riêng cho từng button footer theo
key
Cài đặt
npm install @libs-ui/components-modalImport
Modal chuẩn
import {
LibsUiComponentsModalComponent,
IModalHeaderConfig,
IModalBodyConfig,
IModalFooterConfig,
IModalFunctionsControl,
TYPE_MODAL_EVENT,
} from '@libs-ui/components-modal';
@Component({
standalone: true,
imports: [LibsUiComponentsModalComponent],
})
export class YourComponent {}Modal V2
import {
LibsUiComponentsModalV2Component,
LibsUiComponentsModalV2AbstractComponent,
IModalV2HeaderConfig,
IModalV2BodyConfig,
IModalV2FooterConfig,
IModalV2FunctionsControl,
IModalV2EventToModal,
TModalV2EventToComponent,
TYPE_MODAL_EVENT,
} from '@libs-ui/components-modal';
import { LibsUiDynamicComponentService, setInputs } from '@libs-ui/services-dynamic-component';Ví dụ sử dụng
Ví dụ 1 — Modal chuẩn: Hộp thoại xác nhận (Center)
// confirm-delete.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsModalComponent,
TYPE_MODAL_EVENT,
} from '@libs-ui/components-modal';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsModalComponent],
templateUrl: './confirm-delete.component.html',
})
export class ConfirmDeleteComponent {
protected isVisible = signal(false);
protected handlerOpenConfirm(event: Event) {
event.stopPropagation();
this.isVisible.set(true);
}
protected handlerModalEvent(event: TYPE_MODAL_EVENT) {
event; // không có stopPropagation vì đây là Custom Event từ output, không phải DOM Event
if (event === 'agree') {
// Thực hiện xóa
this.isVisible.set(false);
}
if (event === 'close' || event === 'cancel') {
this.isVisible.set(false);
}
}
}<!-- confirm-delete.component.html -->
<button (click)="handlerOpenConfirm($event)">Xóa bản ghi</button>
<libs_ui-components-modal
[(show)]="isVisible"
mode="center"
title="Xác nhận xóa"
[bodyConfig]="{
iconType: 'warning',
lines: [
{ text: 'Bạn có chắc chắn muốn xóa bản ghi này không?' },
{ text: 'Hành động này không thể hoàn tác.', class: 'text-red-500 font-medium mt-2' }
]
}"
[escapeKeyboardCloseModal]="true"
(outEvent)="handlerModalEvent($event)">
</libs_ui-components-modal>Ví dụ 2 — Modal chuẩn: Side Panel với nội dung tùy chỉnh (Offset Right)
// user-detail.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import {
LibsUiComponentsModalComponent,
IModalHeaderConfig,
TYPE_MODAL_EVENT,
} from '@libs-ui/components-modal';
@Component({
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsModalComponent],
templateUrl: './user-detail.component.html',
})
export class UserDetailComponent {
protected isPanelOpen = signal(false);
protected readonly headerConfig: IModalHeaderConfig = {
hasBackButton: true,
};
protected handlerOpen(event: Event) {
event.stopPropagation();
this.isPanelOpen.set(true);
}
protected handlerClose(event: Event) {
event.stopPropagation();
this.isPanelOpen.set(false);
}
protected handlerSave(event: Event) {
event.stopPropagation();
// Xử lý lưu dữ liệu
this.isPanelOpen.set(false);
}
protected handlerModalEvent(modalEvent: TYPE_MODAL_EVENT) {
if (modalEvent === 'close' || modalEvent === 'back' || modalEvent === 'cancel') {
this.isPanelOpen.set(false);
}
}
}<!-- user-detail.component.html -->
<button (click)="handlerOpen($event)">Xem chi tiết người dùng</button>
<libs_ui-components-modal
[(show)]="isPanelOpen"
mode="offset-right"
width="560px"
title="Chi tiết người dùng"
[headerConfig]="headerConfig"
[isClickOutsideClose]="true"
(outEvent)="handlerModalEvent($event)">
<div class="libs-ui-modal-body-custom p-6">
<div class="flex items-center gap-4 mb-6">
<div class="w-14 h-14 rounded-full bg-blue-100 flex items-center justify-center text-blue-600 font-bold text-xl">AD</div>
<div>
<p class="libs-ui-font-h3m">Admin User</p>
<p class="libs-ui-font-h5r text-gray-500">[email protected]</p>
</div>
</div>
<div class="space-y-3">
<p class="libs-ui-font-h5r text-gray-600">Nội dung chi tiết người dùng ở đây...</p>
</div>
</div>
<div class="libs-ui-modal-footer-custom flex justify-end gap-3 px-6 py-4">
<libs_ui-components-buttons-button
type="button-third"
label="Hủy"
(outClick)="handlerClose($event)">
</libs_ui-components-buttons-button>
<libs_ui-components-buttons-button
label="Lưu thay đổi"
(outClick)="handlerSave($event)">
</libs_ui-components-buttons-button>
</div>
</libs_ui-components-modal>Ví dụ 3 — Modal V2: Mở động từ TypeScript với Lazy Load Body
// order-management.component.ts
import { ChangeDetectionStrategy, Component, ComponentRef, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
LibsUiComponentsModalV2Component,
IModalV2BodyConfig,
IModalV2FunctionsControl,
IModalV2EventToModal,
} from '@libs-ui/components-modal';
import { LibsUiDynamicComponentService, setInputs } from '@libs-ui/services-dynamic-component';
import { from } from 'rxjs';
// Định nghĩa kiểu sự kiện giao tiếp (TOut) — PHẢI extends IModalV2EventToModal
interface IOrderFormEventOut extends IModalV2EventToModal<'SUBMITTED' | 'CANCELLED'> {
eventName: 'SUBMITTED' | 'CANCELLED';
orderId?: string;
}
@Component({
selector: 'app-order-management',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<libs_ui-components-buttons-button label="Tạo đơn hàng mới" (outClick)="handlerOpenModal($event)">
</libs_ui-components-buttons-button>
`,
})
export class OrderManagementComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private readonly destroyRef = inject(DestroyRef);
private activeModalRef?: ComponentRef<LibsUiComponentsModalV2Component<never, IOrderFormEventOut>>;
private readonly bodyConfig: IModalV2BodyConfig<unknown> = {
classInclude: 'p-0',
// Lazy load component — chỉ tải khi modal mở
getComponentOutlet: () =>
from(import('./order-form/order-form.component').then((c) => c.OrderFormComponent)),
};
protected handlerOpenModal(event: Event) {
event.stopPropagation();
this.activeModalRef = this.dynamicService.resolveComponentFactory(
LibsUiComponentsModalV2Component
);
setInputs(this.activeModalRef, {
title: 'Tạo đơn hàng mới',
mode: 'offset-right',
width: '600px',
bodyConfig: this.bodyConfig,
buttonsFooter: {
right: [
{
key: 'submit',
label: 'Tạo đơn hàng',
action: async (fc: IModalV2FunctionsControl<never, IOrderFormEventOut>) => {
// Gửi lệnh submit xuống component con
fc.getEventToComponentStream().next({
groupEvent: 'custom',
eventName: 'submit' as never,
});
},
},
{
label: 'Hủy',
type: 'button-third',
action: async (fc: IModalV2FunctionsControl<never, IOrderFormEventOut>) => fc.hide(),
},
],
},
show: true,
disable: false,
});
// Cleanup khi modal đóng
this.activeModalRef.instance.outClose
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dynamicService.delete(this.activeModalRef);
this.activeModalRef = undefined;
});
this.dynamicService.addToBody(this.activeModalRef);
// Lắng nghe sự kiện từ component con
const fc = this.activeModalRef.instance.FunctionsControl;
fc.getEventToModalStream()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (event.eventName === 'SUBMITTED') {
console.log('Đơn hàng đã được tạo:', event.orderId);
fc.hide();
}
});
}
}Ví dụ 4 — Modal V2: Component con kế thừa Abstract class
// order-form/order-form.component.ts
import { ChangeDetectionStrategy, Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import {
LibsUiComponentsModalV2AbstractComponent,
IModalV2EventToModal,
} from '@libs-ui/components-modal';
// Interface sự kiện gửi "VỀ" cho Modal cha
interface IOrderFormEventOut extends IModalV2EventToModal<'SUBMITTED' | 'CANCELLED'> {
eventName: 'SUBMITTED' | 'CANCELLED';
orderId?: string;
}
@Component({
selector: 'app-order-form',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="p-6">
<p class="libs-ui-font-h5r text-gray-600 mb-4">Nội dung form tạo đơn hàng...</p>
<button (click)="handlerSubmit($event)">Submit nội bộ</button>
</div>
`,
})
export class OrderFormComponent extends LibsUiComponentsModalV2AbstractComponent<never, IOrderFormEventOut> {
private readonly destroyRef = inject(DestroyRef);
constructor() {
super();
// Lắng nghe các sự kiện hệ thống từ Modal cha
this.eventToComponent()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (event.groupEvent === 'hidden' && event.eventName === 'cancel') {
// Modal đang đóng — cleanup nếu cần
}
});
}
protected handlerSubmit(event: Event) {
event.stopPropagation();
// Gửi sự kiện về Modal cha
this.eventToModal().next({
eventName: 'SUBMITTED',
orderId: 'ORD-2024-001',
});
}
}Ví dụ 5 — Modal V2: Loading button khi thao tác bất đồng bộ
// Trong parent component — mở modal với button có loading state
this.activeModalRef = this.dynamicService.resolveComponentFactory(LibsUiComponentsModalV2Component);
setInputs(this.activeModalRef, {
title: 'Xác nhận thao tác',
mode: 'center',
width: '480px',
buttonsFooter: {
right: [
{
key: 'save',
label: 'Lưu thay đổi',
action: async (fc: IModalV2FunctionsControl<never, null>) => {
// Bật loading — button 'save' hiển thị spinner, toàn bộ modal bị disable
await fc.setStateLoadingButton('save', true);
try {
await apiService.save(formData).toPromise();
} finally {
// Tắt loading dù thành công hay lỗi
await fc.setStateLoadingButton('save', false);
fc.hide();
}
},
},
{
label: 'Hủy',
type: 'button-third',
action: async (fc: IModalV2FunctionsControl<never, null>) => fc.hide(),
},
],
},
show: true,
disable: false,
});
this.activeModalRef.instance.outClose.subscribe(() => {
this.dynamicService.delete(this.activeModalRef);
});
this.dynamicService.addToBody(this.activeModalRef);@Input() — LibsUiComponentsModalComponent (Modal chuẩn)
Lưu ý:
LibsUiComponentsModalComponentđược đánh dấu@deprecated. Hãy dùngLibsUiComponentsModalV2Componentcho các tính năng mới.
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [(show)] | boolean | true | Two-way binding — điều khiển trạng thái hiển thị | [(show)]="isVisible" |
| [mode] | 'center' \| 'offset-right' | 'offset-right' | Vị trí hiển thị của modal | mode="center" |
| [title] | string | '' | Tiêu đề hiển thị trên header | title="Xác nhận xóa" |
| [headerConfig] | IModalHeaderConfig | {} | Cấu hình chi tiết cho phần header | [headerConfig]="{ hasBackButton: true }" |
| [bodyConfig] | IModalBodyConfig | {} | Cấu hình nội dung mặc định phần body | [bodyConfig]="{ iconType: 'warning', lines: [...] }" |
| [footerConfig] | IModalFooterConfig | {} | Cấu hình phần footer | [footerConfig]="{ hidden: true }" |
| [buttonsFooter] | Array<IButton> | [Cancel, Agree] | Danh sách buttons footer (model) — modal chuẩn dùng array | [buttonsFooter]="myButtons" |
| [(width)] | string | undefined | Chiều rộng của modal | [(width)]="'600px'" |
| [(height)] | string | undefined | Chiều cao của modal | [(height)]="'80vh'" |
| [(maxWidth)] | string | undefined | Chiều rộng tối đa | [maxWidth]="'900px'" |
| [(maxHeight)] | string | undefined | Chiều cao tối đa | [maxHeight]="'90vh'" |
| [(minWidth)] | string | undefined | Chiều rộng tối thiểu | [minWidth]="'400px'" |
| [zIndex] | number | 1200 | Thứ tự lớp hiển thị CSS | [zIndex]="1500" |
| [(disable)] | boolean | false | Vô hiệu hóa toàn bộ tương tác trong modal | [(disable)]="isProcessing" |
| [isFullScreen] | boolean | undefined | Bật chế độ toàn màn hình — tự động set width/height 100vw/100vh | [isFullScreen]="true" |
| [escapeKeyboardCloseModal] | boolean | false | Đóng modal khi nhấn phím Escape | [escapeKeyboardCloseModal]="true" |
| [isClickOutsideClose] | boolean | false | Đóng modal khi click ra ngoài vùng modal | [isClickOutsideClose]="true" |
| [isBackdropTransparent] | boolean | undefined | Làm trong suốt lớp backdrop mờ | [isBackdropTransparent]="true" |
| [isBackgroundTransparentModal] | boolean | undefined | Làm trong suốt nền của modal | [isBackgroundTransparentModal]="true" |
| [isSizeBackdropByWidthHeightInput] | boolean | undefined | Backdrop co giãn theo width/height của modal thay vì 100vw/100vh | [isSizeBackdropByWidthHeightInput]="true" |
| [hasShadowBoxWhenHiddenBackDropTransparent] | boolean | undefined | Hiển thị bóng đổ khi backdrop trong suốt | [hasShadowBoxWhenHiddenBackDropTransparent]="true" |
| [classIncludeModalWrapper] | string | 'libs-ui-modal-wrapper' | Class CSS bổ sung cho container bao ngoài | [classIncludeModalWrapper]="'my-modal-wrapper'" |
| [ignoreCommunicateMicroEvent] | boolean | undefined | Bỏ qua việc gửi PostMessage tới parent window (dùng trong micro-frontend) | [ignoreCommunicateMicroEvent]="true" |
| [titleUseXssFilter] | boolean | undefined | Bật bộ lọc XSS cho tiêu đề (mặc định đã có) | [titleUseXssFilter]="true" |
| [titleUseTooltip] | boolean | undefined | Hiển thị tooltip khi tiêu đề bị cắt ngắn | [titleUseTooltip]="true" |
| [titleUseInnerText] | boolean | undefined | Render tiêu đề dưới dạng innerText thuần (không parse HTML) | [titleUseInnerText]="true" |
@Output() — LibsUiComponentsModalComponent (Modal chuẩn)
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outEvent) | TYPE_MODAL_EVENT | Phát ra khi người dùng tương tác: close, back, agree, cancel | handlerModalEvent(event: TYPE_MODAL_EVENT): void { if (event === 'close') this.isVisible.set(false); } | (outEvent)="handlerModalEvent($event)" |
| (outClose) | void | Phát ra khi modal bắt đầu đóng (đi kèm cùng outEvent close/back/cancel) | handlerClose(): void { this.cleanup(); } | (outClose)="handlerClose()" |
| (outScrollContent) | Event | Phát ra khi người dùng cuộn nội dung bên trong body | handlerScroll(event: Event): void { event.stopPropagation(); } | (outScrollContent)="handlerScroll($event)" |
| (outFunctionControl) | IModalFunctionsControl | Cung cấp đối tượng điều khiển modal từ bên ngoài | handlerFunctionControl(fc: IModalFunctionsControl): void { this.fc = fc; } | (outFunctionControl)="handlerFunctionControl($event)" |
@Input() — LibsUiComponentsModalV2Component (Modal V2)
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [headerConfig] | IModalV2HeaderConfig | {} | Cấu hình vùng header — hỗ trợ lazy-load component qua getComponentOutlet | [headerConfig]="{ hasBackButton: true }" |
| [bodyConfig] | IModalV2BodyConfig | {} | Cấu hình vùng body — hỗ trợ lazy-load component qua getComponentOutlet | [bodyConfig]="bodyConfig" |
| [footerConfig] | IModalV2FooterConfig | {} | Cấu hình vùng footer — hỗ trợ lazy-load component qua getComponentOutlet | [footerConfig]="{ classInclude: 'bg-gray-50' }" |
| [title] | string | '' | Tiêu đề hiển thị trên header | title="Tạo đơn hàng" |
| [mode] | 'center' \| 'offset-right' | 'offset-right' | Vị trí hiển thị của modal | mode="center" |
| [(show)] | boolean | true | Two-way binding — trạng thái hiển thị | [(show)]="isOpen" |
| [(disable)] | boolean | false | Vô hiệu hóa tương tác — tự động bật khi button đang loading | [(disable)]="isDisabled" |
| [(buttonsFooter)] | { left?: IButton[]; center?: IButton[]; right?: IButton[] } | { right: [Cancel, Agree] } | Buttons footer theo 3 vùng — V2 dùng object thay vì array | [(buttonsFooter)]="footerButtons" |
| [(width)] | string | undefined | Chiều rộng của modal | [width]="'600px'" |
| [(height)] | string | undefined | Chiều cao của modal | [height]="'80vh'" |
| [(maxWidth)] | string | undefined | Chiều rộng tối đa | [maxWidth]="'900px'" |
| [(maxHeight)] | string | undefined | Chiều cao tối đa | [maxHeight]="'90vh'" |
| [(minWidth)] | string | undefined | Chiều rộng tối thiểu | [minWidth]="'400px'" |
| [zIndex] | number | 1200 | Thứ tự lớp hiển thị CSS | [zIndex]="1500" |
| [isFullScreen] | boolean | undefined | Bật chế độ toàn màn hình | [isFullScreen]="true" |
| [escapeKeyboardCloseModal] | boolean | false | Đóng modal khi nhấn phím Escape (tự động vô hiệu khi disable=true) | [escapeKeyboardCloseModal]="true" |
| [isClickOutsideClose] | boolean | false | Đóng modal khi click ra ngoài (tự động vô hiệu khi disable=true) | [isClickOutsideClose]="true" |
| [isBackdropTransparent] | boolean | undefined | Làm trong suốt lớp backdrop | [isBackdropTransparent]="true" |
| [isBackgroundTransparentModal] | boolean | undefined | Làm trong suốt nền modal | [isBackgroundTransparentModal]="true" |
| [isSizeBackdropByWidthHeightInput] | boolean | undefined | Backdrop theo size modal | [isSizeBackdropByWidthHeightInput]="true" |
| [hasShadowBoxWhenHiddenBackDropTransparent] | boolean | undefined | Bóng đổ khi backdrop trong suốt | [hasShadowBoxWhenHiddenBackDropTransparent]="true" |
| [classIncludeModalWrapper] | string | 'libs-ui-modal-wrapper' | Class CSS bổ sung cho container bao ngoài | [classIncludeModalWrapper]="'custom-wrapper'" |
| [ignoreCommunicateMicroEvent] | boolean | undefined | Bỏ qua PostMessage tới parent window | [ignoreCommunicateMicroEvent]="true" |
| [titleUseXssFilter] | boolean | undefined | Bật lọc XSS cho tiêu đề | [titleUseXssFilter]="true" |
| [titleUseTooltip] | boolean | undefined | Tooltip khi tiêu đề bị cắt | [titleUseTooltip]="true" |
| [titleUseInnerText] | boolean | undefined | Render tiêu đề dạng innerText thuần | [titleUseInnerText]="true" |
@Output() — LibsUiComponentsModalV2Component (Modal V2)
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outEvent) | TYPE_MODAL_EVENT | Phát ra khi tương tác: close, back, agree, cancel | handlerEvent(event: TYPE_MODAL_EVENT): void { if (event === 'close') this.cleanup(); } | (outEvent)="handlerEvent($event)" |
| (outClose) | void | Phát ra khi modal bắt đầu đóng | handlerClose(): void { this.dynamicService.delete(this.modalRef); } | (outClose)="handlerClose()" |
| (outScrollContent) | Event | Phát ra khi cuộn nội dung body | handlerScroll(event: Event): void { event.stopPropagation(); } | (outScrollContent)="handlerScroll($event)" |
Lưu ý Modal V2: Không có
outFunctionControl. Truy cậpFunctionsControltrực tiếp qua getter:this.activeModalRef.instance.FunctionsControl.
FunctionsControl — IModalV2FunctionsControl
Getter FunctionsControl trả về đối tượng điều khiển modal sau khi addToBody(). Sử dụng để điều khiển show/hide và giao tiếp 2 chiều qua Subject stream.
| Method | Signature | Mô tả |
|---|---|---|
| show() | () => Promise<void> | Hiển thị modal |
| hide() | () => Promise<void> | Ẩn modal |
| setStateDisable(state) | (state: boolean) => Promise<void> | Cập nhật trạng thái disable của modal |
| setStateLoadingButton(key, state) | (key: string, state: boolean) => Promise<void> | Bật/tắt spinner cho button theo key. Khi true — modal tự động bị disable |
| getEventToComponentStream() | () => Subject<TModalV2EventToComponent<TIn, TInData>> | Lấy Subject để gửi sự kiện xuống component con |
| getEventToModalStream() | () => Subject<EnsureLiteralEventType<TOut>> | Lấy Subject để nhận sự kiện từ component con |
// Cách lấy FunctionsControl (sau khi addToBody):
const fc = this.activeModalRef.instance.FunctionsControl;
// Gửi sự kiện xuống component con
fc.getEventToComponentStream().next({
groupEvent: 'custom',
eventName: 'RELOAD_DATA',
data: { refreshId: 42, triggerSource: 'SaveButton' },
});
// Lắng nghe sự kiện từ component con
fc.getEventToModalStream()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((event) => {
if (event.eventName === 'SUBMITTED') {
fc.hide();
}
});
// Loading button trong footer action
action: async (fc) => {
await fc.setStateLoadingButton('submit', true);
await apiService.save().toPromise();
await fc.setStateLoadingButton('submit', false);
fc.hide();
}FunctionsControl — IModalFunctionsControl (Modal chuẩn)
| Method | Signature | Mô tả |
|---|---|---|
| show() | () => Promise<void> | Hiển thị modal |
| hide() | () => Promise<void> | Ẩn modal |
| setStateDisable(state) | (state: boolean) => Promise<void> | Cập nhật trạng thái disable |
// Nhận FunctionsControl từ outFunctionControl:
protected fc?: IModalFunctionsControl;
handlerFunctionControl(fc: IModalFunctionsControl): void {
this.fc = fc;
}
// Sử dụng:
this.fc?.hide();
this.fc?.setStateDisable(true);Content Projection — Modal chuẩn
Modal chuẩn hỗ trợ tiêm nội dung tùy chỉnh qua 3 slot content projection:
| Slot (class CSS) | Vị trí | Mô tả |
|---|---|---|
| div.libs-ui-modal-header-custom | Header | Nội dung tùy chỉnh thêm vào header |
| div.libs-ui-modal-body-custom | Body | Nội dung chính của modal |
| div.libs-ui-modal-footer-custom | Footer | Nội dung footer tùy chỉnh thay thế buttons mặc định |
<libs_ui-components-modal [(show)]="isOpen" title="Form tùy chỉnh">
<!-- Body tùy chỉnh -->
<div class="libs-ui-modal-body-custom p-6">
<form>
<input type="text" placeholder="Nhập tên..." class="w-full border rounded p-2" />
</form>
</div>
<!-- Footer tùy chỉnh (thay thế buttons mặc định) -->
<div class="libs-ui-modal-footer-custom flex justify-end gap-3 px-6 py-4">
<libs_ui-components-buttons-button type="button-third" label="Hủy" (outClick)="handlerCancel($event)">
</libs_ui-components-buttons-button>
<libs_ui-components-buttons-button label="Xác nhận" (outClick)="handlerConfirm($event)">
</libs_ui-components-buttons-button>
</div>
</libs_ui-components-modal>Types & Interfaces
import {
TYPE_MODAL_EVENT,
IModalHeaderConfig,
IModalBodyConfig,
IModalBodyLineConfig,
IModalFooterConfig,
IModalFunctionsControl,
IModalV2HeaderConfig,
IModalV2BodyConfig,
IModalV2FooterConfig,
IModalV2FunctionsControl,
IModalV2EventToModal,
TModalV2EventToComponent,
IModalV2ChildComponent,
} from '@libs-ui/components-modal';TYPE_MODAL_EVENT
type TYPE_MODAL_EVENT = 'close' | 'cancel' | 'back' | 'agree';IModalHeaderConfig
interface IModalHeaderConfig {
classInclude?: string; // Class CSS bổ sung cho header
hasBackButton?: boolean; // Hiển thị nút quay lại (trái header)
removeButtonClose?: boolean; // Ẩn nút X đóng modal
classButtonCloseInclude?: string; // Class CSS bổ sung cho nút X
hidden?: boolean; // Ẩn toàn bộ header
classTitle?: string; // Class CSS bổ sung cho tiêu đề
ignoreHeaderTheme?: boolean; // Bỏ qua theme mặc định của header
}IModalBodyConfig
interface IModalBodyConfig {
classInclude?: string; // Class CSS bổ sung cho body
hidden?: boolean; // Ẩn toàn bộ body
lines?: Array<IModalBodyLineConfig>; // Danh sách dòng text hiển thị trong body
iconType?: 'warning' | 'success' | 'fail' | 'information'; // Icon theo loại
classIcon?: string; // Class tùy chỉnh cho icon (thay thế icon mặc định)
scrollOverlayOptions?: IScrollOverlayOptions; // Cấu hình scroll overlay
}
interface IModalBodyLineConfig {
text: string; // Nội dung dòng text
class?: string; // Class CSS bổ sung
useXssFilter?: boolean; // Bật lọc XSS cho nội dung (mặc định true)
}IModalFooterConfig
interface IModalFooterConfig {
classInclude?: string; // Class CSS bổ sung cho footer
buttonClassIncludeCustom?: string; // Class tùy chỉnh cho vùng chứa buttons
hidden?: boolean; // Ẩn toàn bộ footer
footerCustomClass?: string; // Class cho slot nội dung tùy chỉnh
}IModalFunctionsControl (Modal chuẩn)
interface IModalFunctionsControl {
show: () => Promise<void>;
hide: () => Promise<void>;
setStateDisable: (stateDisable: boolean) => Promise<void>;
}IModalV2HeaderConfig / IModalV2BodyConfig / IModalV2FooterConfig (Modal V2)
// Kế thừa từ IModalHeaderConfig / IModalBodyConfig / IModalFooterConfig, bổ sung:
interface IModalV2BodyConfig<TDataInput> extends IModalBodyConfig {
// Hàm trả về Observable<Type<Component>> — lazy load component vào body
getComponentOutlet?: () => Observable<any>;
// Hàm trả về Observable với dữ liệu truyền vào component con
getDataComponentOutlet?: () => Observable<TDataInput>;
// WritableSignal để kiểm soát skeleton loading (reactive).
// KHÔNG truyền -> dùng skeleton mặc định: 1 khối trải kín toàn bộ body (full-body).
skeletonConfig?: WritableSignal<ISkeletonConfig>;
}
// IModalV2HeaderConfig và IModalV2FooterConfig có cùng cấu trúcIModalV2FunctionsControl (Modal V2)
interface IModalV2FunctionsControl<TIn = never, TOut extends { eventName: string } | null = null, TInData = unknown>
extends IModalFunctionsControl {
getEventToComponentStream: () => Subject<TModalV2EventToComponent<TIn, TInData>>;
getEventToModalStream: () => Subject<EnsureLiteralEventType<TOut>>;
setStateLoadingButton: (key: string, state: boolean) => Promise<void>;
}TModalV2EventToComponent (sự kiện "vào" component con)
type TModalV2EventToComponent<T = never, TData = unknown> =
| {
groupEvent: 'show' | 'hidden' | 'agree';
eventName: 'show' | 'hide' | 'back' | 'close' | 'agree' | 'cancel' | 'scroll' | 'disable';
event?: Event;
stateDisable?: boolean;
keyButtonClick?: string; // Key button đang gây ra disable
}
| {
groupEvent: 'custom';
eventName: T; // Custom event name (type-safe với generic TIn)
data?: TData; // Payload tùy chỉnh (type-safe với generic TInData)
};IModalV2EventToModal (interface cho sự kiện từ component con "về" modal cha)
interface IModalV2EventToModal<T extends string> {
eventName: T; // Phải là string literal (VD: 'SUBMIT'), không phải 'string' chung chung
}LibsUiComponentsModalV2AbstractComponent (Abstract class cho component con)
Component con được load vào modal V2 BẮT BUỘC kế thừa class này để nhận các input/stream chuẩn:
// Kế thừa trong component con:
export class MyBodyComponent extends LibsUiComponentsModalV2AbstractComponent<TIn, TOut, TInData> {
// Tự động nhận 3 required inputs:
// - eventToComponent: InputSignal<Subject<TModalV2EventToComponent<TIn, TInData>>>
// - eventToModal: InputSignal<Subject<TOut>>
// - functionControlModal: InputSignal<IModalV2FunctionsControl<TIn, TOut, TInData>>
}Luồng sự kiện Modal V2
Parent Component
│
├── resolveComponentFactory(LibsUiComponentsModalV2Component)
├── setInputs(modalRef, { bodyConfig: { getComponentOutlet: () => ... }, ... })
├── addToBody(modalRef)
│
│ ┌─────────────────────────────────────────────┐
│ │ LibsUiComponentsModalV2Component │
│ │ │
│ │ [eventToComponent Subject] ──────────► │ ──► Child Component
│ │ │
│ │ [eventToModal Subject] ◄────────── │ ◄── Child Component
│ │ │
│ │ FunctionsControl.getEventToModalStream() │
│ │ FunctionsControl.getEventToComponentStream()│
│ └─────────────────────────────────────────────┘
│
├── fc.getEventToComponentStream().next({ groupEvent: 'custom', eventName: 'MY_CMD', data: {...} })
└── fc.getEventToModalStream().subscribe(event => { if (event.eventName === 'DONE') fc.hide() })Lưu ý quan trọng
⚠️ Deprecated Modal chuẩn: LibsUiComponentsModalComponent đã được đánh dấu @deprecated. Dùng LibsUiComponentsModalV2Component cho mọi tính năng mới.
⚠️ Cleanup bắt buộc với Modal V2: Luôn gọi this.dynamicService.delete(this.activeModalRef) sau khi modal đóng để tránh memory leak. Subscribe vào outClose để thực hiện cleanup tự động.
this.activeModalRef.instance.outClose
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.dynamicService.delete(this.activeModalRef);
this.activeModalRef = undefined;
});⚠️ Khai báo ComponentRef cấp class: activeModalRef phải là thuộc tính của class (không phải biến local trong hàm) để có thể cleanup đúng cách.
// ❌ SAI — local variable, không cleanup được
handlerOpen() {
const ref = this.dynamicService.resolveComponentFactory(LibsUiComponentsModalV2Component);
}
// ✅ ĐÚNG — class property
private activeModalRef?: ComponentRef<LibsUiComponentsModalV2Component<...>>;⚠️ getComponentOutlet phải trả về Observable<Type<Component>>: Dùng from(import(...).then(m => m.ComponentClass)) để lazy load đúng chuẩn. Không truyền instance.
⚠️ skeletonConfig phải là WritableSignal<ISkeletonConfig>: Không phải object thường. Skeleton chỉ reactive khi dùng signal.
ℹ️ Skeleton mặc định = full-body: Nếu không truyền skeletonConfig, Modal V2 dùng skeleton mặc định là 1 khối trải kín toàn bộ chiều cao body (repeat: 1, item w-full h-full). Body mặc định là display:flex; flex-direction:column khiến skeleton host (flex item) co về cao 0 → cần đổi body sang display:block để h-full có mốc neo chiều cao — ví dụ truyền classInclude: '!block !p-0' cho bodyConfig (xem demo "Skeleton Full Body").
⚠️ eventName trong TOut bắt buộc là string literal: Không dùng string, any, unknown. TypeScript sẽ báo lỗi compile nếu vi phạm.
⚠️ Thứ tự sau addToBody: Chỉ truy cập FunctionsControl và subscribe streams sau khi đã gọi this.dynamicService.addToBody(this.activeModalRef).
⚠️ disable và loading button: Khi gọi setStateLoadingButton('key', true), modal tự động bị disable=true — phím Escape và click outside sẽ không hoạt động cho đến khi gọi setStateLoadingButton('key', false).
⚠️ Không dùng .pipe() trên OutputEmitterRef: outClose, outEvent, outScrollContent là OutputEmitterRef — không kế thừa từ Observable. Dùng .subscribe() trực tiếp hoặc wrap với fromOutputEmitter() nếu cần pipe.
Demo
npx nx serve core-uiTruy cập:
- Modal chuẩn: http://localhost:4500/components/modal
- Modal V2: http://localhost:4500/components/modal-v2
