@libs-ui/services-dynamic-component
v0.2.357-9
Published
> Service tạo và quản lý Angular component động tại runtime — gắn vào body, element cụ thể, hoặc parent document trong kịch bản iframe.
Downloads
3,004
Readme
@libs-ui/services-dynamic-component
Service tạo và quản lý Angular component động tại runtime — gắn vào body, element cụ thể, hoặc parent document trong kịch bản iframe.
Giới thiệu
LibsUiDynamicComponentService cung cấp API đầy đủ để tạo Angular component bằng code TypeScript thuần (không khai báo trong template HTML), đính kèm vào bất kỳ vị trí nào trên DOM, và dọn dẹp đúng cách khi không còn cần thiết. Ngoài ra, package còn xuất kèm hai hàm tiện ích setInputs / setInput giúp truyền input type-safe vào ComponentRef đã tạo.
Tính năng
- Tạo Angular component động qua
createComponentAPI (Angular 14+) - Gắn component vào
document.body, element bất kỳ, hoặc element theoid - Hỗ trợ kịch bản iframe: gắn component vào
window.parent.document.bodyvà tự động sao chép style sang parent document - Cache DOM element theo
idbằngMapđể tránh query DOM nhiều lần - Xóa component khỏi DOM với tuỳ chọn: xóa ngay, delay destroy, hoặc chỉ detach (giữ lại để reuse)
- Hàm
setInputs/setInputtruyền input type-safe vàoComponentRef - Singleton (
providedIn: 'root') — dùng xuyên suốt ứng dụng
Khi nào sử dụng
- Tạo modal, dialog, toast, tooltip động từ TypeScript mà không cần khai báo trong template
- Gắn component vào vị trí DOM tuỳ ý (outside Angular host tree)
- Trong kịch bản micro-frontend / iframe: cần render component ở parent window
- Cần kiểm soát lifecycle component động (create → attach → detach → destroy)
- Dùng kèm với
LibsUiComponentsModalV2Componenthoặc các template page component
Cài đặt
npm install @libs-ui/services-dynamic-componentImport
import { LibsUiDynamicComponentService, setInputs, setInput } from '@libs-ui/services-dynamic-component';Ví dụ sử dụng
Ví dụ 1 — Tạo component và gắn vào document.body
import { Component, ComponentRef, inject } from '@angular/core';
import { LibsUiDynamicComponentService } from '@libs-ui/services-dynamic-component';
import { MyToastComponent } from './my-toast.component';
@Component({
selector: 'app-dashboard',
standalone: true,
template: `<button (click)="handlerShowToast($event)">Hiện toast</button>`,
})
export class DashboardComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private toastRef?: ComponentRef<MyToastComponent>;
handlerShowToast(event: Event): void {
event.stopPropagation();
this.toastRef = this.dynamicService.resolveComponentFactory(MyToastComponent);
this.dynamicService.addToBody(this.toastRef);
}
handlerHideToast(event: Event): void {
event.stopPropagation();
this.dynamicService.delete(this.toastRef);
this.toastRef = undefined;
}
}Ví dụ 2 — Gắn component vào element có id cụ thể
import { Component, ComponentRef, inject } from '@angular/core';
import { LibsUiDynamicComponentService } from '@libs-ui/services-dynamic-component';
import { MyPreviewComponent } from './my-preview.component';
@Component({
selector: 'app-file-list',
standalone: true,
template: `
<div id="preview-panel" class="min-h-[200px] border rounded-lg"></div>
<button (click)="handlerOpenPreview($event)">Xem trước</button>
`,
})
export class FileListComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private previewRef?: ComponentRef<MyPreviewComponent>;
handlerOpenPreview(event: Event): void {
event.stopPropagation();
this.previewRef = this.dynamicService.resolveComponentFactory(MyPreviewComponent);
this.dynamicService.addToIdAttributeElement(this.previewRef, 'preview-panel');
}
handlerClosePreview(event: Event): void {
event.stopPropagation();
this.dynamicService.delete(this.previewRef);
this.previewRef = undefined;
}
}Ví dụ 3 — Truyền input type-safe với setInputs
import { Component, ComponentRef, inject } from '@angular/core';
import { LibsUiDynamicComponentService, setInputs } from '@libs-ui/services-dynamic-component';
import { LibsUiComponentsModalV2Component } from '@libs-ui/components-modal';
import { ExtractInputs } from '@libs-ui/interfaces-types';
import { from, of } from 'rxjs';
import { MyModalBodyComponent, IMyModalBodyInputs } from './my-modal-body.component';
@Component({
selector: 'app-order-list',
standalone: true,
template: `<button (click)="handlerOpenModal($event)">Mở modal</button>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class OrderListComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private modalRef?: ComponentRef<LibsUiComponentsModalV2Component>;
handlerOpenModal(event: Event): void {
event.stopPropagation();
this.modalRef = this.dynamicService.resolveComponentFactory(LibsUiComponentsModalV2Component);
setInputs(this.modalRef, {
bodyConfig: {
component: () => from(import('./my-modal-body.component').then((m) => m.MyModalBodyComponent)),
getDataComponentOutlet: () => of<ExtractInputs<IMyModalBodyInputs>>({ orderId: '123', mode: 'view' }),
},
});
this.dynamicService.addToBody(this.modalRef);
}
handlerCloseModal(event: Event): void {
event.stopPropagation();
this.dynamicService.delete(this.modalRef);
this.modalRef = undefined;
}
}Ví dụ 4 — Gắn component vào parent document (kịch bản iframe)
import { Component, ComponentRef, inject } from '@angular/core';
import { LibsUiDynamicComponentService } from '@libs-ui/services-dynamic-component';
import { MyOverlayComponent } from './my-overlay.component';
@Component({
selector: 'app-iframe-content',
standalone: true,
template: `<button (click)="handlerOpenOverlay($event)">Mở overlay ở parent</button>`,
})
export class IframeContentComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private overlayRef?: ComponentRef<MyOverlayComponent>;
handlerOpenOverlay(event: Event): void {
event.stopPropagation();
// isAddParentDocument = true: gắn vào window.parent.document.body
// Styles từ iframe tự động được sao chép sang parent document (delay 250ms)
this.overlayRef = this.dynamicService.resolveComponentFactory(MyOverlayComponent);
this.dynamicService.addToBody(this.overlayRef, true);
}
handlerCloseOverlay(event: Event): void {
event.stopPropagation();
// Khi delete: styles đã sao chép cũng được dọn sạch khỏi parent document
this.dynamicService.delete(this.overlayRef);
this.overlayRef = undefined;
}
}Ví dụ 5 — Xóa component với delay (dùng cho animation)
import { Component, ComponentRef, inject } from '@angular/core';
import { LibsUiDynamicComponentService } from '@libs-ui/services-dynamic-component';
import { MyAnimatedComponent } from './my-animated.component';
@Component({
selector: 'app-animated-wrapper',
standalone: true,
template: `<button (click)="handlerDismiss($event)">Đóng (có animation)</button>`,
})
export class AnimatedWrapperComponent {
private readonly dynamicService = inject(LibsUiDynamicComponentService);
private panelRef?: ComponentRef<MyAnimatedComponent>;
handlerOpen(event: Event): void {
event.stopPropagation();
this.panelRef = this.dynamicService.resolveComponentFactory(MyAnimatedComponent);
this.dynamicService.addToBody(this.panelRef);
}
handlerDismiss(event: Event): void {
event.stopPropagation();
// detachView() ngay lập tức, gọi destroy() sau 300ms để animation kết thúc
this.dynamicService.delete(this.panelRef, { timeoutDestroy: 300 });
this.panelRef = undefined;
}
}Methods
LibsUiDynamicComponentService
| Method | Signature | Mô tả |
|---|---|---|
| resolveComponentFactory | (component: Type<T>): ComponentRef<T> | Tạo ComponentRef từ class component. Sử dụng createComponent API với injector của ApplicationRef. |
| addToBody | (componentRef?: ComponentRef<any>, isAddParentDocument?: boolean, timerDelayUpdateStyleToParent?: number): void | Gắn component vào document.body. Nếu isAddParentDocument = true, gắn vào window.parent.document.body và sao chép styles (mặc định delay 250ms). |
| addToElement | (componentRef: any, elementAdd: HTMLElement): HTMLElement \| undefined | Gắn component vào element DOM bất kỳ. |
| addToElementLayoutContentDefault | (componentRef: any, id?: string): void | Gắn component vào element có id bằng idElementLayoutContentDefault (mặc định "libs-ui-layout-content"). |
| addToIdAttributeElement | (componentRef: any, id: string): HTMLElement \| undefined | Gắn component vào element có id chỉ định. Element được cache trong Map — lần sau không query DOM lại. |
| delete | (componentRef: any, options?: { ignoreDestroyComponent?: boolean; timeoutDestroy?: number }): void | Detach view khỏi ApplicationRef, dọn sạch styles parent document (nếu có), rồi destroy component. |
| remove | (componentRef: any, _?: string, ignoreDestroyComponent?: boolean): void | Deprecated — dùng delete() thay thế. |
delete() — Options chi tiết
| Option | Type | Mô tả |
|---|---|---|
| ignoreDestroyComponent | boolean | Chỉ detach view, không gọi destroy(). Dùng khi muốn tái sử dụng component sau. |
| timeoutDestroy | number | Gọi destroy() sau N milliseconds. Detach view xảy ra ngay lập tức. Dùng cho animation close. |
IdElementLayoutContentDefault (setter)
// Thay đổi id mặc định cho addToElementLayoutContentDefault()
this.dynamicService.IdElementLayoutContentDefault = 'my-custom-layout-id';Hàm tiện ích — setInputs / setInput
setInputs
Truyền nhiều input cùng lúc vào ComponentRef với type-safety đầy đủ nhờ ExtractInputs.
import { setInputs } from '@libs-ui/services-dynamic-component';
import { ExtractInputs } from '@libs-ui/interfaces-types';
const modalRef = this.dynamicService.resolveComponentFactory(MyModalComponent);
setInputs(modalRef, {
title: 'Xác nhận xoá',
confirmLabel: 'Xoá',
itemId: 'abc-123',
} as ExtractInputs<MyModalComponent>);Signature đầy đủ:
setInputs<T, R extends boolean = true>(
componentRef: ComponentRef<T>,
inputs: ExtractInputs<T, R>,
required?: R
): voidsetInput
Truyền từng input riêng lẻ, an toàn với keyof.
import { setInput } from '@libs-ui/services-dynamic-component';
const panelRef = this.dynamicService.resolveComponentFactory(MyPanelComponent);
setInput(panelRef, 'title', 'Chi tiết đơn hàng');
setInput(panelRef, 'orderId', 'ORD-456');Signature đầy đủ:
setInput<T>(
componentRef: ComponentRef<T>,
input: keyof ExtractInputs<T>,
value: ExtractInputs<T>[keyof ExtractInputs<T>]
): voidTypes & Interfaces
Package sử dụng ExtractInputs từ @libs-ui/interfaces-types:
import { ExtractInputs } from '@libs-ui/interfaces-types';
// Dùng để type-check khi gọi setInputs()
type T_MyComponentInputs = ExtractInputs<MyComponent>;Lưu ý quan trọng
⚠️ Singleton & Shared State: Service là providedIn: 'root' — toàn bộ ứng dụng dùng chung một instance. elementLayoutContent Map và elementBody được cache và chia sẻ giữa tất cả usages. Tránh nhiều component cùng ghi vào cùng một container id đồng thời nếu không kiểm soát được thứ tự.
⚠️ Stale Cache: Nếu element có id bị xóa khỏi DOM sau khi đã được cache, addToIdAttributeElement vẫn giữ reference cũ và có thể không append được. Cần tạo lại element hoặc xóa cache bằng cách khởi động lại flow.
⚠️ Memory Leak: Luôn gọi delete(componentRef) khi component không còn dùng đến. Không gọi destroy() sẽ khiến component tồn tại trong memory. Lưu ComponentRef là class property (không phải local variable) để có thể cleanup trong ngOnDestroy.
⚠️ Styles Parent Document: Khi dùng isAddParentDocument = true, styles được clone và copy sang parent document. Khi gọi delete() — styles sẽ được tự động dọn sạch. Nếu service bị destroy mà không gọi delete() trước — styles trong parent document vẫn còn và phải dọn thủ công.
⚠️ remove() Deprecated: Phương thức remove() đã bị đánh dấu @deprecated. Luôn dùng delete() thay thế.
⚠️ ComponentRef phải là class property: Lưu ComponentRef ở cấp class (không phải const trong hàm) để có thể gọi delete() sau này và tránh memory leak.
// ❌ SAI — local variable, không thể cleanup
handlerOpen() {
const ref = this.dynamicService.resolveComponentFactory(MyComponent);
this.dynamicService.addToBody(ref);
}
// ✅ ĐÚNG — class property, dọn được trong ngOnDestroy
private panelRef?: ComponentRef<MyComponent>;
handlerOpen(event: Event) {
event.stopPropagation();
this.panelRef = this.dynamicService.resolveComponentFactory(MyComponent);
this.dynamicService.addToBody(this.panelRef);
}
ngOnDestroy() {
this.dynamicService.delete(this.panelRef);
}