@libs-ui/services-dialog
v0.2.357-32
Published
> Service quản lý Modal Dialog động trong Angular — tạo, hiển thị và đóng Dialog mà không cần khai báo thẻ trong template.
Readme
@libs-ui/services-dialog
Service quản lý Modal Dialog động trong Angular — tạo, hiển thị và đóng Dialog mà không cần khai báo thẻ trong template.
Giới thiệu
LibsUiDialogService cung cấp API đơn giản để hiển thị Modal Dialog tại bất kỳ đâu trong ứng dụng Angular thông qua Service, không cần khai báo component trong template HTML. Service quản lý toàn bộ vòng đời của Dialog — từ khởi tạo component động, gắn vào DOM, xử lý các event (agree/cancel/close/back), đến dọn dẹp khi đóng. Mỗi Dialog được định danh bằng UUID, cho phép quản lý nhiều Dialog đồng thời một cách độc lập.
Tính năng
- ✅ Tạo Modal Dialog động tại runtime (không cần khai báo trong template)
- ✅ Quản lý nhiều Dialog đồng thời qua Map với UUID unique
- ✅ Xử lý 4 loại event:
agree,cancel,close,backvới callback async - ✅ Hỗ trợ tùy chỉnh footer với danh sách button tùy ý (
buttonsFooter) - ✅ Auto-close sau event hoặc kiểm soát thủ công qua
ignoreRemoveDialog - ✅ Toggle disable trạng thái toàn bộ Dialog để ngăn thao tác trùng lặp
- ✅ Hỗ trợ cả Micro Frontend (
isAddParentDocument) - ✅ Bảo mật XSS mặc định cho title (
titleUseXssFilter: true) - ✅ Hai chế độ vị trí:
centervàoffset-right
Khi nào sử dụng
- Khi cần hiển thị Dialog/Modal từ service layer mà không muốn khai báo component trong template
- Khi cần thông báo xác nhận (
confirm) trước các thao tác destructive (xóa, reset, gửi) - Khi cần Dialog với async action (gọi API, loading state) trước khi đóng
- Khi cần tạo Dialog linh hoạt với nhiều loại button tùy chỉnh ở footer
- Khi cần quản lý nhiều Dialog đồng thời và có thể đóng từ bất kỳ component nào
Cài đặt
npm install @libs-ui/services-dialogImport
import { LibsUiDialogService } from '@libs-ui/services-dialog';
import { IDialog, IDialogConfigEvent } from '@libs-ui/services-dialog';Vì là Service (providedIn: 'root'), không cần khai báo trong imports[] của component. Chỉ cần inject:
import { Component, inject } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-my-feature',
templateUrl: './my-feature.component.html',
})
export class MyFeatureComponent {
private readonly dialogService = inject(LibsUiDialogService);
}Ví dụ sử dụng
1. Dialog thông báo cơ bản
import { Component, inject } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-notification-demo',
template: `<button (click)="handlerOpenNotice()">Thông báo</button>`,
})
export class NotificationDemoComponent {
private readonly dialogService = inject(LibsUiDialogService);
protected handlerOpenNotice(): void {
const dialogId = this.dialogService.addDialog({
title: 'Thông báo',
bodyConfig: {
lines: [{ text: 'Cập nhật dữ liệu thành công.' }],
},
buttonsFooter: [
{
label: 'Đóng',
type: 'button-primary',
action: async () => {
this.dialogService.removeDialog(dialogId);
await Promise.resolve();
},
},
],
});
}
}2. Dialog xác nhận xóa với async action
import { Component, inject } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-delete-confirm',
template: `<button (click)="handlerDeleteItem()">Xóa</button>`,
})
export class DeleteConfirmComponent {
private readonly dialogService = inject(LibsUiDialogService);
protected handlerDeleteItem(): void {
const dialogId = this.dialogService.addDialog({
title: 'Xác nhận xóa',
width: '400px',
bodyConfig: {
iconType: 'warning',
lines: [
{ text: 'Bạn có chắc chắn muốn xóa mục này không?' },
{ text: 'Hành động này không thể hoàn tác.', class: 'text-red-500' },
],
},
buttonsFooter: [
{
label: 'Hủy bỏ',
type: 'button-primary-revert',
action: async () => {
this.dialogService.removeDialog(dialogId);
await Promise.resolve();
},
},
{
label: 'Xóa ngay',
type: 'button-danger-high',
action: async () => {
await this.deleteApi();
this.dialogService.removeDialog(dialogId);
},
},
],
});
}
private async deleteApi(): Promise<void> {
// Gọi API xóa dữ liệu
}
}3. Dialog với loading state — ngăn auto-close
import { Component, inject } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-processing-dialog',
template: `<button (click)="handlerSubmit()">Gửi dữ liệu</button>`,
})
export class ProcessingDialogComponent {
private readonly dialogService = inject(LibsUiDialogService);
protected handlerSubmit(): void {
const dialogId = this.dialogService.addDialog({
title: 'Xác nhận gửi',
bodyConfig: {
lines: [{ text: 'Bạn muốn gửi dữ liệu lên hệ thống?' }],
},
configAgreeEvent: {
ignoreRemoveDialog: true, // Ngăn auto-close để chờ xử lý xong
callback: async (control) => {
control?.setStateDisable(true); // Disable nút tránh click nhiều lần
try {
await this.submitApi();
this.dialogService.removeDialog(dialogId); // Đóng thủ công sau khi xong
} catch {
control?.setStateDisable(false); // Re-enable khi lỗi
}
},
},
configCancelEvent: {
callback: async () => {
await Promise.resolve();
},
},
});
}
private async submitApi(): Promise<void> {
// Gọi API submit dữ liệu
}
}4. Dialog với nhiều Dialog đồng thời và clearAll
import { Component, inject, signal } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-multi-dialog',
template: `
<button (click)="handlerOpenMultiple()">Mở nhiều Dialog</button>
<button (click)="handlerClearAll()">Đóng tất cả</button>
`,
})
export class MultiDialogComponent {
private readonly dialogService = inject(LibsUiDialogService);
private readonly openDialogIds = signal<string[]>([]);
protected handlerOpenMultiple(): void {
const id = this.dialogService.addDialog({
title: `Dialog #${this.openDialogIds().length + 1}`,
bodyConfig: {
lines: [{ text: 'Dialog được mở tại runtime.' }],
},
zIndex: 1000 + this.openDialogIds().length * 10,
buttonsFooter: [
{
label: 'Đóng',
type: 'button-primary',
action: async () => {
this.dialogService.removeDialog(id);
await Promise.resolve();
},
},
],
});
this.openDialogIds.update((ids) => [...ids, id]);
}
protected handlerClearAll(): void {
this.dialogService.clearDialogsRef();
this.openDialogIds.set([]);
}
}5. Dialog dạng offset-right (panel bên phải)
import { Component, inject } from '@angular/core';
import { LibsUiDialogService } from '@libs-ui/services-dialog';
@Component({
standalone: true,
selector: 'app-side-panel',
template: `<button (click)="handlerOpenPanel()">Mở panel</button>`,
})
export class SidePanelComponent {
private readonly dialogService = inject(LibsUiDialogService);
protected handlerOpenPanel(): void {
const dialogId = this.dialogService.addDialog({
title: 'Chi tiết',
mode: 'offset-right',
width: '480px',
height: '100%',
bodyConfig: {
lines: [{ text: 'Nội dung chi tiết hiển thị dạng panel bên phải.' }],
},
configCloseEvent: {
callback: async () => {
await Promise.resolve();
},
},
});
}
}Methods
| Method | Signature | Mô tả |
|---|---|---|
| addDialog | (config: IDialog, isAddParentDocument?: boolean) => string | Tạo và hiển thị một Dialog mới. Trả về dialogId (UUID) để tham chiếu sau này |
| removeDialog | (id: string) => void | Đóng và xóa Dialog theo ID. Bỏ qua nếu ID không tồn tại |
| clearDialogsRef | () => void | Đóng và xóa tất cả Dialog đang mở |
| switchDisableActionsOnDialog | (id: string) => void | Toggle trạng thái disable của Dialog theo ID (false → true → false) |
Types & Interfaces
import { IDialog, IDialogConfigEvent } from '@libs-ui/services-dialog';IDialog
Cấu hình truyền vào addDialog():
| Property | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| title | string | - | Tiêu đề của Dialog | title: 'Xác nhận xóa' |
| bodyConfig | IModalBodyConfig | - | Cấu hình nội dung Dialog (lines, icon, class) | bodyConfig: { lines: [{ text: 'Nội dung' }] } |
| buttonsFooter | IButton[] | - | Danh sách button tùy chỉnh ở footer. Khi có, footer mặc định bị thay thế hoàn toàn | buttonsFooter: [{ label: 'OK', type: 'button-primary', action: async () => {} }] |
| width | string | '500px' | Độ rộng của Dialog | width: '400px' |
| height | string | 'auto' | Chiều cao của Dialog | height: '600px' |
| maxWidth | string | - | Chiều rộng tối đa | maxWidth: '90vw' |
| maxHeight | string | - | Chiều cao tối đa | maxHeight: '80vh' |
| mode | 'center' \| 'offset-right' | 'center' | Vị trí hiển thị Dialog | mode: 'offset-right' |
| zIndex | number | - | Thứ tự lớp hiển thị | zIndex: 1050 |
| disable | boolean | false | Khóa toàn bộ tương tác trên Dialog | disable: true |
| titleUseInnerText | boolean | false | Dùng innerText cho title (bảo mật, ngăn HTML injection) | titleUseInnerText: true |
| titleUseXssFilter | boolean | true | Bật bộ lọc XSS cho title (mặc định bật) | titleUseXssFilter: false |
| classIncludeModalWrapper | string | - | Class CSS thêm vào wrapper ngoài cùng của Modal | classIncludeModalWrapper: 'custom-dialog' |
| headerConfig | IModalHeaderConfig | { hidden: !title } | Cấu hình header (ẩn/hiện, style). Mặc định ẩn nếu không có title | headerConfig: { hidden: false } |
| footerConfig | IModalFooterConfig | - | Cấu hình footer container | footerConfig: { classInclude: 'justify-start' } |
| configAgreeEvent | IDialogConfigEvent | - | Callback khi nhấn nút Đồng ý (Agree). Không hoạt động khi có buttonsFooter | xem bên dưới |
| configCancelEvent | IDialogConfigEvent | - | Callback khi nhấn nút Hủy (Cancel). Không hoạt động khi có buttonsFooter | xem bên dưới |
| configCloseEvent | IDialogConfigEvent | - | Callback khi nhấn nút Close (X) ở header | xem bên dưới |
| configBackEvent | IDialogConfigEvent | - | Callback khi nhấn nút Back | xem bên dưới |
| ignoreCommunicateMicroEvent | boolean | - | Bỏ qua xử lý event giao tiếp Micro Frontend | ignoreCommunicateMicroEvent: true |
IDialogConfigEvent
import { IDialogConfigEvent } from '@libs-ui/services-dialog';
// Cấu trúc interface
interface IDialogConfigEvent {
callback: (functionsControl?: IModalFunctionsControl) => Promise<void>;
ignoreRemoveDialog?: boolean; // true = Dialog không tự đóng sau event
}Ví dụ sử dụng:
const config: IDialogConfigEvent = {
ignoreRemoveDialog: true,
callback: async (control) => {
control?.setStateDisable(true); // Disable nút trong khi xử lý
await someAsyncOperation();
control?.setStateDisable(false);
},
};IModalBodyConfig (từ @libs-ui/components-modal)
import { IModalBodyConfig, IModalBodyLineConfig } from '@libs-ui/components-modal';
// Cấu trúc interface
interface IModalBodyConfig {
classInclude?: string; // Class CSS thêm vào body container
hidden?: boolean; // Ẩn/hiện body
lines?: IModalBodyLineConfig[]; // Danh sách dòng nội dung
iconType?: 'warning' | 'success' | 'fail' | 'information'; // Icon phía trên nội dung
classIcon?: string; // Class tùy chỉnh cho icon
}
interface IModalBodyLineConfig {
text: string; // Nội dung dòng text
class?: string; // Class CSS cho dòng này
useXssFilter?: boolean; // Bật lọc XSS cho text này
}Ví dụ với icon và nhiều dòng:
this.dialogService.addDialog({
title: 'Cảnh báo',
bodyConfig: {
iconType: 'warning',
lines: [
{ text: 'Thao tác này có thể ảnh hưởng đến dữ liệu hiện tại.' },
{ text: 'Vui lòng kiểm tra kỹ trước khi tiếp tục.', class: 'text-gray-500 text-sm' },
],
},
});IModalFunctionsControl (từ @libs-ui/components-modal)
Nhận được qua tham số control trong callback của config event:
interface IModalFunctionsControl {
show: () => Promise<void>; // Hiển thị Modal
hide: () => Promise<void>; // Ẩn Modal (không xóa)
setStateDisable: (stateDisable: boolean) => Promise<void>; // Bật/tắt disable state
}Lưu ý quan trọng
⚠️ buttonsFooter Override — configEvent KHÔNG hoạt động: Khi truyền buttonsFooter, footer mặc định bị thay thế hoàn toàn. Các button trong buttonsFooter chỉ gọi button.action(), không emit event agree/cancel. Do đó, configAgreeEvent và configCancelEvent sẽ không bao giờ được gọi. Chỉ configCloseEvent vẫn hoạt động vì được trigger bởi nút Close (X) ở header (không phải footer). Khuyến nghị: khi dùng buttonsFooter, xử lý toàn bộ logic trong button.action().
⚠️ Auto-close mặc định: Dialog tự đóng ngay sau khi callback của config event chạy xong (trừ khi ignoreRemoveDialog: true). Nếu cần chờ async operation xong mới đóng, set ignoreRemoveDialog: true và gọi removeDialog(id) thủ công.
⚠️ Singleton Service: Service là providedIn: 'root', tất cả component dùng chung một instance và cùng dialogsRef Map. dialogId trả về từ addDialog() có thể dùng để đóng Dialog từ bất kỳ component nào có biết ID đó.
⚠️ setStateDisable trong callback: Khi nhận event (agree/cancel/close/back), service tự động gọi functionControl.setStateDisable(true) trước khi gọi callback. Không cần gọi lại trong callback trừ khi muốn re-enable (setStateDisable(false)).
⚠️ isAddParentDocument cho Micro Frontend: Tham số thứ 2 của addDialog(). Khi true, Dialog sẽ được gắn vào parentDocument thay vì document hiện tại — dùng trong môi trường Micro Frontend để Dialog hiển thị đúng layer.
