@libs-ui/components-pages-template-detail
v0.2.357-4
Published
> Template layout chuẩn cho trang chi tiết — tích hợp header có nút quay lại, tiêu đề, và thanh action phía phải với nhiều loại control.
Readme
@libs-ui/components-pages-template-detail
Template layout chuẩn cho trang chi tiết — tích hợp header có nút quay lại, tiêu đề, và thanh action phía phải với nhiều loại control.
Giới thiệu
@libs-ui/components-pages-template-detail cung cấp hai component khung layout dành cho trang chi tiết (detail page): V1 (LibsUiComponentsPagesTemplateDetailComponent) và V2 (LibsUiComponentsPagesTemplateDetailV2Component). Cả hai đều bao gồm header chuẩn hóa với nút quay lại, tiêu đề có popover, và vùng action bên phải hỗ trợ nhiều loại control (button, switch, dropdown, radio group, tooltip-button). V2 bổ sung khả năng lazy-load component vào vùng body qua bodyConfig, skeleton loading, và ChangeDetectionStrategy.OnPush.
⚠️ Deprecated:
LibsUiComponentsPagesTemplateDetailComponent(V1) đã bị deprecated. Ưu tiên sử dụngLibsUiComponentsPagesTemplateDetailV2Component(V2) cho mọi trang mới.
Tính năng
- ✅ Header chuẩn hóa: Nút quay lại (chevron icon), nhãn "Quay lại danh sách", tiêu đề với popover tooltip khi hover.
- ✅ Thanh action phải linh hoạt: Hỗ trợ
button,swicth,menu-dropdown,button-dropdown,radio-group,tooltip-button,tooltip,circle-and-numbertrên cùng một thanh header. - ✅ Header tùy chỉnh tiêu đề phụ (header.content): V2 hỗ trợ dòng tiêu đề lớn phía trên dải header chính.
- ✅ Chia tỉ lệ header 24/52/24: Bật
isSplitHeaderRatiođể header chia đều ba vùng trái — giữa — phải. - ✅ Vùng body lazy-load (V2): Lazy load component vào body qua
bodyConfig.getComponentOutlet, hiển thị skeleton khi chưa resolve. - ✅ Skeleton loading (V2): Skeleton tùy chỉnh hoàn toàn qua
bodyConfig.skeletonConfig. - ✅ Scroll overlay: Tích hợp sẵn
LibsUiComponentsScrollOverlayDirective, emit eventoutScrollkhi cuộn body. - ✅ FunctionControl API: Expose
setStateDisable()quaoutFunctionControlđể component cha điều khiển trạng thái disable từ bên ngoài. - ✅ XSS-safe: Tiêu đề và mô tả tự động escape HTML qua
LibsUiPipesEscapeHtmlPipe. - ✅ Angular Signals + OnPush (V2): Toàn bộ input dùng Signal API, V2 có
ChangeDetectionStrategy.OnPush.
Khi nào sử dụng
- Khi xây dựng trang chi tiết thực thể (Chi tiết khách hàng, Chi tiết đơn hàng, Chi tiết hợp đồng...).
- Cần header thống nhất có đầy đủ action: nút Lưu, nút Hủy, dropdown thao tác, switch trạng thái.
- Trang chi tiết có body là một component riêng cần lazy-load (dùng V2 +
bodyConfig). - Khi muốn hiển thị tiêu đề trang ở giữa header (ví dụ: tên record đang xem), dùng
isSplitHeaderRatio+configCenter.
Cài đặt
npm install @libs-ui/components-pages-template-detailImport
// V2 (khuyến nghị cho code mới)
import { LibsUiComponentsPagesTemplateDetailV2Component } from '@libs-ui/components-pages-template-detail';
// V1 (deprecated — chỉ dùng khi maintain code cũ)
import { LibsUiComponentsPagesTemplateDetailComponent } from '@libs-ui/components-pages-template-detail';
// Interfaces & Types
import {
IPagesTemplateDetailConfigTitle,
IPagesTemplateDetailConfigRight,
IPagesTemplateDetailConfigCenter,
IPageDetailFunctionControl,
IPageDetailV2BodyConfig,
IPageDetailV2SectionData,
} from '@libs-ui/components-pages-template-detail';Ví dụ sử dụng
Ví dụ 1 — Trang chi tiết cơ bản (V2)
// customer-detail.component.ts
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { LibsUiComponentsPagesTemplateDetailV2Component } from '@libs-ui/components-pages-template-detail';
import { IPageDetailFunctionControl, IPagesTemplateDetailConfigRight } from '@libs-ui/components-pages-template-detail';
@Component({
selector: 'app-customer-detail',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPagesTemplateDetailV2Component],
template: `
<libs_ui-components-pages_template-detail_v2
[configTitle]="{
config: { content: 'Nguyễn Văn An' },
isShowBackToListLabel: true
}"
[configRight]="configRight"
(outClose)="handlerClose($event)"
(outFunctionControl)="handlerFunctionControl($event)">
</libs_ui-components-pages_template-detail_v2>
`,
})
export class CustomerDetailComponent {
protected configRight: IPagesTemplateDetailConfigRight[] = [
{
key: 'button',
configButton: {
label: 'Lưu thay đổi',
type: 'button-primary',
action: () => this.handlerSave(),
},
},
{
key: 'button',
configButton: {
label: 'Hủy',
type: 'button-secondary',
action: () => this.handlerCancel(),
},
},
];
private functionControl?: IPageDetailFunctionControl;
protected handlerClose(event: Event): void {
event.stopPropagation();
// Navigate back to list
}
protected handlerFunctionControl(control: IPageDetailFunctionControl): void {
this.functionControl = control;
}
private handlerSave(): void {
// Save logic
}
private handlerCancel(): void {
// Cancel logic
}
}Ví dụ 2 — Header chia 3 phần (24/52/24) với tiêu đề ở giữa
// order-detail.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LibsUiComponentsPagesTemplateDetailV2Component } from '@libs-ui/components-pages-template-detail';
import {
IPageDetailFunctionControl,
IPagesTemplateDetailConfigCenter,
IPagesTemplateDetailConfigRight,
} from '@libs-ui/components-pages-template-detail';
@Component({
selector: 'app-order-detail',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPagesTemplateDetailV2Component],
template: `
<libs_ui-components-pages_template-detail_v2
[isSplitHeaderRatio]="true"
[configTitle]="{
config: { content: 'Mã đơn: #DH-2024-001' },
isShowBackToListLabel: true
}"
[configCenter]="configCenter"
[configRight]="configRight"
(outClose)="handlerClose($event)"
(outFunctionControl)="handlerFunctionControl($event)">
</libs_ui-components-pages_template-detail_v2>
`,
})
export class OrderDetailComponent {
protected configCenter: IPagesTemplateDetailConfigCenter = {
title: 'Chi tiết đơn hàng',
classIncludeTitle: 'uppercase libs-ui-font-h4s',
};
protected configRight: IPagesTemplateDetailConfigRight[] = [
{
key: 'swicth',
configSwicth: {
active: true,
action: async (event) => {
// Handle switch change
},
},
},
{
key: 'menu-dropdown',
configDropdown: {
listConfig: [
{ key: 'export', label: 'Xuất PDF' },
{ key: 'print', label: 'In đơn hàng' },
{ key: 'delete', label: 'Xóa đơn hàng' },
],
},
},
];
protected handlerClose(event: Event): void {
event.stopPropagation();
// Navigate back
}
protected handlerFunctionControl(control: IPageDetailFunctionControl): void {
// Store function control for external use
}
}Ví dụ 3 — Lazy-load body component (V2 + bodyConfig)
// contract-detail.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { from, of } from 'rxjs';
import { LibsUiComponentsPagesTemplateDetailV2Component } from '@libs-ui/components-pages-template-detail';
import {
IPageDetailFunctionControl,
IPageDetailV2BodyConfig,
IPagesTemplateDetailConfigRight,
} from '@libs-ui/components-pages-template-detail';
@Component({
selector: 'app-contract-detail',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPagesTemplateDetailV2Component],
template: `
<libs_ui-components-pages_template-detail_v2
[configTitle]="{
config: { content: 'Hợp đồng số: HD-2024-001' },
isShowBackToListLabel: true
}"
[configRight]="configRight"
[bodyConfig]="bodyConfig"
(outClose)="handlerClose($event)"
(outFunctionControl)="handlerFunctionControl($event)">
</libs_ui-components-pages_template-detail_v2>
`,
})
export class ContractDetailComponent {
protected bodyConfig: IPageDetailV2BodyConfig = {
getComponentOutlet: () =>
from(
import('./contract-info/contract-info.component').then(
(m) => m.ContractInfoComponent
)
),
getDataComponentOutlet: (sectionData) =>
of({
contractId: 'HD-2024-001',
disable: sectionData?.disable ?? false,
}),
classInclude: 'p-[24px]',
};
protected configRight: IPagesTemplateDetailConfigRight[] = [
{
key: 'button',
configButton: {
label: 'Duyệt hợp đồng',
type: 'button-primary',
action: () => this.handlerApprove(),
},
},
];
protected handlerClose(event: Event): void {
event.stopPropagation();
// Navigate back
}
protected handlerFunctionControl(control: IPageDetailFunctionControl): void {
// Dùng control.setStateDisable(true) để disable toàn bộ action trong header
}
private handlerApprove(): void {
// Approve logic
}
}Ví dụ 4 — Điều khiển trạng thái disable từ bên ngoài qua FunctionControl
// invoice-detail.component.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { LibsUiComponentsPagesTemplateDetailV2Component } from '@libs-ui/components-pages-template-detail';
import { IPageDetailFunctionControl, IPagesTemplateDetailConfigRight } from '@libs-ui/components-pages-template-detail';
@Component({
selector: 'app-invoice-detail',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsPagesTemplateDetailV2Component],
template: `
<libs_ui-components-pages_template-detail_v2
[configTitle]="{ config: { content: 'Hóa đơn #INV-001' } }"
[configRight]="configRight"
(outClose)="handlerClose($event)"
(outFunctionControl)="handlerFunctionControl($event)"
(outStateDisable)="handlerStateDisable($event)">
</libs_ui-components-pages_template-detail_v2>
`,
})
export class InvoiceDetailComponent {
protected configRight: IPagesTemplateDetailConfigRight[] = [
{
key: 'button',
configButton: {
label: 'Lưu',
type: 'button-primary',
action: () => this.handlerSave(),
},
},
];
private functionControl?: IPageDetailFunctionControl;
protected handlerClose(event: Event): void {
event.stopPropagation();
}
protected handlerFunctionControl(control: IPageDetailFunctionControl): void {
this.functionControl = control;
}
protected handlerStateDisable(isDisabled: boolean): void {
// React when disable state changes
}
private async handlerSave(): Promise<void> {
// Disable all header actions while saving
await this.functionControl?.setStateDisable(true);
try {
// ... save API call
} finally {
await this.functionControl?.setStateDisable(false);
}
}
}@Input() — LibsUiComponentsPagesTemplateDetailV2Component
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [bodyConfig] | IPageDetailV2BodyConfig | {} | Cấu hình lazy-load component vào vùng body. Khi không truyền getComponentOutlet, vùng body sẽ không render (dùng ng-content thay thế với V1). | [bodyConfig]="{ getComponentOutlet: () => from(import('./body.component').then(m => m.BodyComponent)) }" |
| [classIncludeHeader] | string | undefined | CSS class bổ sung cho dải header. | [classIncludeHeader]="'border-b-2 border-blue-500'" |
| [configCenter] | IPagesTemplateDetailConfigCenter | undefined | Cấu hình vùng giữa header — chỉ hiển thị khi isSplitHeaderRatio là true. | [configCenter]="{ title: 'Tên bản ghi', classIncludeTitle: 'uppercase libs-ui-font-h4s' }" |
| [configRight] | Array<IPagesTemplateDetailConfigRight> | undefined | Danh sách control hiển thị ở bên phải header, theo thứ tự từ trái sang phải. | [configRight]="[{ key: 'button', configButton: { label: 'Lưu', type: 'button-primary' } }]" |
| [configTitle] | IPagesTemplateDetailConfigTitle | undefined | Cấu hình vùng bên trái header: nút quay lại, tiêu đề popover, mô tả phụ, tiêu đề phụ trên cùng. | [configTitle]="{ config: { content: 'Nguyễn Văn An' }, isShowBackToListLabel: true }" |
| [disable] | boolean (model — two-way) | false | Trạng thái disable toàn bộ action trong header. Có thể bind two-way [(disable)]. | [(disable)]="isDisabled" |
| [isSplitHeaderRatio] | boolean | undefined | Khi true, header chia thành 3 vùng với tỉ lệ 24% — 52% — 24%; vùng giữa hiển thị configCenter. | [isSplitHeaderRatio]="true" |
| [zIndex] | number | 1000 | Giá trị z-index CSS của toàn bộ component. Tự động fallback về 1000 khi truyền undefined. | [zIndex]="1200" |
@Input() — LibsUiComponentsPagesTemplateDetailComponent (V1, deprecated)
Tương tự V2, ngoại trừ không có bodyConfig. V1 có thêm [classIncludeBody] để thêm class cho vùng body khi dùng ng-content.
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [classIncludeBody] | string | undefined | CSS class bổ sung cho vùng body (chỉ V1, dùng với ng-content). | [classIncludeBody]="'p-[24px]'" |
| [classIncludeHeader] | string | undefined | CSS class bổ sung cho dải header. | [classIncludeHeader]="'shadow-sm'" |
| [configCenter] | IPagesTemplateDetailConfigCenter | undefined | Cấu hình vùng giữa header. | [configCenter]="{ title: 'Tên trang' }" |
| [configRight] | Array<IPagesTemplateDetailConfigRight> | undefined | Danh sách control phía phải header. | [configRight]="actionList" |
| [configTitle] | IPagesTemplateDetailConfigTitle | undefined | Cấu hình vùng trái header. | [configTitle]="{ config: { content: 'Tiêu đề' } }" |
| [disable] | boolean (model) | false | Trạng thái disable toàn bộ action. | [(disable)]="isDisabled" |
| [isSplitHeaderRatio] | boolean | undefined | Chia header theo tỉ lệ 24/52/24. | [isSplitHeaderRatio]="true" |
| [zIndex] | number | 1000 | Giá trị z-index của component. | [zIndex]="1100" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outClose) | boolean | Phát ra true khi người dùng click nút quay lại (chevron icon hoặc nhãn "Quay lại danh sách"). | handlerClose(event: Event): void { event.stopPropagation(); /* navigate back */ } | (outClose)="handlerClose($event)" |
| (outFunctionControl) | IPageDetailFunctionControl | Phát ra object chứa các hàm điều khiển component từ bên ngoài (emit một lần trong ngOnInit). | handlerFunctionControl(ctrl: IPageDetailFunctionControl): void { this.control = ctrl; } | (outFunctionControl)="handlerFunctionControl($event)" |
| (outScroll) | Event | Phát ra native scroll event khi người dùng cuộn vùng body. | handlerScroll(event: Event): void { event.stopPropagation(); /* handle scroll */ } | (outScroll)="handlerScroll($event)" |
| (outSelectedButtonDropdown) | IEmitSelectKey | Phát ra khi người dùng chọn một item từ button-dropdown trong configRight. | handlerSelectedButtonDropdown(event: IEmitSelectKey): void { event.stopPropagation(); /* handle select */ } | (outSelectedButtonDropdown)="handlerSelectedButtonDropdown($event)" |
| (outSelectedMenuDropdown) | IEmitSelectKey \| undefined | Phát ra khi người dùng chọn một item từ menu-dropdown trong configRight. | handlerSelectedMenuDropdown(event: IEmitSelectKey \| undefined): void { event.stopPropagation(); /* handle select */ } | (outSelectedMenuDropdown)="handlerSelectedMenuDropdown($event)" |
| (outSelectedRadio) | IRadioEvent | Phát ra khi người dùng thay đổi lựa chọn trong radio-group trên header. | handlerSelectedRadio(event: IRadioEvent): void { event.stopPropagation(); /* handle change */ } | (outSelectedRadio)="handlerSelectedRadio($event)" |
| (outStateDisable) | boolean | Phát ra giá trị disable mới mỗi khi setStateDisable() được gọi qua FunctionControl. | handlerStateDisable(isDisabled: boolean): void { /* react to disable change */ } | (outStateDisable)="handlerStateDisable($event)" |
| (outTooltipButtonFunctionControl) | IPopoverFunctionControlEvent | Phát ra function control của popover/tooltip gắn với tooltip-button trong configRight. | handlerTooltipControl(event: IPopoverFunctionControlEvent): void { this.tooltipControl = event; } | (outTooltipButtonFunctionControl)="handlerTooltipControl($event)" |
Types & Interfaces
import {
IPagesTemplateDetailConfigTitle,
IPagesTemplateDetailConfigRight,
IPagesTemplateDetailConfigCenter,
IPageDetailFunctionControl,
IPageDetailV2BodyConfig,
IPageDetailV2SectionData,
} from '@libs-ui/components-pages-template-detail';IPagesTemplateDetailConfigTitle
import { IPopover, IPopoverOverlay } from '@libs-ui/components-popover';
interface IPagesTemplateDetailConfigTitle extends IPopover {
/** Tiêu đề phụ hiển thị dạng dải riêng phía trên header chính (chỉ V2) */
header?: {
content?: string;
classInclude?: string;
};
/** Ẩn nút mũi tên quay lại (chevron icon). Mặc định: false */
ignoreButtonBack?: boolean;
/**
* Tắt escape HTML tự động cho nội dung tiêu đề.
* Chỉ dùng khi nội dung đã được sanitize từ nguồn tin cậy.
* Mặc định: false (luôn escape)
*/
ignoreEscapeHtml?: boolean;
/** Hiển thị mũi tên (arrow begin) trước nút quay lại */
isShowArrowBegin?: boolean;
/** Hiển thị nhãn "Quay lại danh sách" dạng link button thay vì icon */
isShowBackToListLabel?: boolean;
/** Cấu hình mô tả phụ hiển thị bên cạnh tiêu đề chính */
configDescription?: {
innerView?: string;
config: IPopoverOverlay;
};
}IPagesTemplateDetailConfigRight
import { IButton } from '@libs-ui/components-buttons-button';
import { IButtonDropdown } from '@libs-ui/components-buttons-dropdown';
import { IDropdown } from '@libs-ui/components-dropdown';
import { IPopover } from '@libs-ui/components-popover';
import { IRadioGroupItem } from '@libs-ui/components-radio-group';
import { ISwitch } from '@libs-ui/components-switch';
type ButtonKey =
| 'button'
| 'swicth'
| 'radio-group'
| 'circle-and-number'
| 'button-dropdown'
| 'menu-dropdown'
| 'tooltip-button'
| 'tooltip';
interface IPagesTemplateDetailConfigRight {
/** Loại control cần hiển thị */
key: ButtonKey;
/** CSS class bổ sung cho wrapper của control này */
classInclude?: string;
/** Trạng thái disable riêng cho control này (ưu tiên hơn disable toàn component) */
disable?: boolean;
/** Hiển thị loading spinner trên control */
isPending?: boolean;
/** Ẩn control này (dùng để điều kiện hiện/ẩn mà không cần unmount) */
ignoreShowButton?: boolean;
/** Cấu hình cho key: 'button' */
configButton?: IButton;
/** Cấu hình cho key: 'button-dropdown' */
configButtonDropdown?: IButtonDropdown;
/** Cấu hình cho key: 'radio-group' */
configRadioGroup?: Array<IRadioGroupItem>;
/** Cấu hình cho key: 'swicth' */
configSwicth?: ISwitch;
/** Cấu hình cho key: 'menu-dropdown' */
configDropdown?: IDropdown;
/** Cấu hình cho key: 'tooltip-button' */
configTooltipButton?: {
configTooltip?: IPopover;
configButton?: IButton;
};
}IPagesTemplateDetailConfigCenter
import { TemplateRef } from '@angular/core';
import { TYPE_TEMPLATE_REF } from '@libs-ui/interfaces-types';
interface IPagesTemplateDetailConfigCenter {
/** Text tiêu đề (tự động translate và escape HTML) */
title?: string;
/** CSS class bổ sung cho phần tử tiêu đề */
classIncludeTitle?: string;
/** TemplateRef tùy chỉnh thay thế hoàn toàn vùng giữa khi cần UI phức tạp hơn */
template?: TemplateRef<TYPE_TEMPLATE_REF>;
}IPageDetailFunctionControl
interface IPageDetailFunctionControl {
/**
* Thay đổi trạng thái disable của toàn bộ action trong header.
* Gọi setStateDisable(true) trước khi submit, setStateDisable(false) sau khi hoàn thành.
*/
setStateDisable: (stateDisable: boolean) => Promise<void>;
}IPageDetailV2BodyConfig (chỉ V2)
import { WritableSignal } from '@angular/core';
import { ISkeletonConfig } from '@libs-ui/components-skeleton';
import { TYPE_FUNCTION } from '@libs-ui/interfaces-types';
interface IPageDetailV2BodyConfig {
/**
* Hàm trả về Observable<Type> để lazy-load component vào vùng body.
* Nhận tham số `sectionData: IPageDetailV2SectionData` (có `disable`).
* Khi chưa resolve, hiển thị skeleton.
*/
getComponentOutlet?: TYPE_FUNCTION<any>;
/**
* Hàm trả về Observable<Record> chứa data truyền vào component được load.
* Nhận tham số `sectionData: IPageDetailV2SectionData`.
*/
getDataComponentOutlet?: TYPE_FUNCTION<Record<string, unknown>>;
/** Cấu hình skeleton hiển thị trong lúc đang lazy-load component */
skeletonConfig?: WritableSignal<ISkeletonConfig>;
/** CSS class bổ sung cho wrapper của vùng body */
classInclude?: string;
}
interface IPageDetailV2SectionData {
/** Trạng thái disable hiện tại của component — dùng để truyền xuống body component */
disable: boolean;
}Lưu ý quan trọng
⚠️ V1 đã deprecated: LibsUiComponentsPagesTemplateDetailComponent (selector libs_ui-components-pages_template-detail) không còn được duy trì. Mọi trang mới BẮT BUỘC dùng LibsUiComponentsPagesTemplateDetailV2Component.
⚠️ Lỗi chính tả swicth: Interface dùng configSwicth (không phải configSwitch) và key 'swicth' — đây là tên gốc trong source, cần giữ đúng chính xác khi sử dụng.
⚠️ bodyConfig chỉ hoạt động với getComponentOutlet: Khi bodyConfig.getComponentOutlet không được truyền, V2 sẽ không render vùng body tự động. Cần dùng ng-content (chỉ V1 hỗ trợ) hoặc truyền đủ cấu hình getComponentOutlet.
⚠️ outFunctionControl emit một lần duy nhất: outFunctionControl chỉ emit trong ngOnInit. Phải lưu giá trị vào biến class để dùng sau: private functionControl?: IPageDetailFunctionControl.
⚠️ XSS tự động: Tiêu đề trong configTitle.config.content mặc định được escape HTML. Nếu cần render HTML thực (ví dụ: i18n có HTML), truyền ignoreEscapeHtml: true — chỉ dùng khi nguồn dữ liệu là i18n key do team kiểm soát.
⚠️ isSplitHeaderRatio + configCenter: configCenter chỉ hiển thị khi isSplitHeaderRatio là true. Truyền configCenter mà không bật isSplitHeaderRatio sẽ không có effect.
⚠️ Dynamic component với V2: Khi dùng V2 làm dynamic component trong Modal hoặc page template khác, BẮT BUỘC khai báo Generic Type rõ ràng cho ComponentRef và gọi .destroy() trong ngOnDestroy của component cha để tránh memory leak.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/ và tìm mục "pages-template-detail" trong menu demo.
