@libs-ui/components-table
v0.2.357-4
Published
> Component hiển thị dữ liệu dạng bảng mạnh mẽ với Virtual Scrolling, Ghim cột, Phân trang và kết nối API trực tiếp.
Downloads
4,506
Readme
@libs-ui/components-table
Component hiển thị dữ liệu dạng bảng mạnh mẽ với Virtual Scrolling, Ghim cột, Phân trang và kết nối API trực tiếp.
Giới thiệu
LibsUiComponentsTableComponent là giải pháp toàn diện để hiển thị danh sách dữ liệu lớn. Tích hợp sẵn Virtual Scrolling (tối ưu hàng nghìn bản ghi), ghim cột (Sticky Columns), collapse/expand row và cơ chế kết nối trực tiếp với API service qua httpRequestData.
Tính năng
- ✅ Ghim cột (Sticky Columns): Cột quan trọng luôn hiển thị qua
headerLeft. - ✅ Virtual Scrolling: Tối ưu hiệu năng cho danh sách hàng nghìn bản ghi.
- ✅ Phân trang: Hỗ trợ
scroll-infinityvàclick-pagination. - ✅ Virtual cột ngang: Ảo hoá render cột ngang với
useScrollMeasureColumn. - ✅ Collapse/Expand Row: Mở rộng xem chi tiết dữ liệu cấp dưới.
- ✅ Checkbox Selection: Chọn một hoặc nhiều hàng.
- ✅ Footer tổng hợp: Footer có thể gọi API riêng, đặt trên hoặc dưới.
- ✅ Bar Buttons: Thanh công cụ xuất hiện khi có row được chọn.
- ✅ Sorting: Sort local hoặc server-side.
- ✅ Angular Signals + OnPush: Hiệu năng cao.
Khi nào sử dụng
- Hiển thị danh sách dữ liệu lớn cần Virtual Scroller.
- Bảng nhiều cột cần ghim cột quan trọng (
headerLeft/headerRight). - Cần Footer tổng hợp, Bar Buttons, hoặc tính năng Collapse/Expand row.
- Kết nối trực tiếp API service qua
httpRequestData.
Cài đặt
npm install @libs-ui/components-tableImport
import { LibsUiComponentsTableComponent } from '@libs-ui/components-table';
@Component({
standalone: true,
imports: [LibsUiComponentsTableComponent],
})
export class YourComponent {}Ví dụ sử dụng
1. Standard Table (Virtual Scrolling + httpRequestData)
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerRight]="headerStandard"
[httpRequestData]="userRequestConfig()"
(outSort)="handlerSort($event)"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig, ISortEvent } from '@libs-ui/components-table';
import { IHttpRequestConfig } from '@libs-ui/services-http-request';
import { IAvatarConfig } from '@libs-ui/components-avatar';
import { signal } from '@angular/core';
import { of } from 'rxjs';
readonly headerStandard: Array<ITableHeaderConfig> = [
{
label: 'ID',
orderby: 'id',
hasSort: true,
ngClass: { 'w-1/5': true },
colTemplateConfig: [{ cssWrapper: 'font-mono text-gray-400', fieldsConfig: [{ field: 'id' }] }],
},
{
label: 'Avatar',
ngClass: { 'w-1/5': true },
colTemplateConfig: [{
cssWrapper: 'justify-center',
fieldsConfig: [{
field: 'staff_info',
instance: 'avatar',
getAvatarConfig: (data) => {
// data.value có thể là Signal (data wrap qua convertObjectToSignal / httpRequestData)
// hoặc object thường → unwrap an toàn; fallback lấy từ row (data.item).
const unwrap = (v: unknown) => (typeof v === 'function' ? (v as () => unknown)() : v);
const staffInfo = unwrap((unwrap(data.item) as Record<string, unknown>)?.['staff_info']) ?? unwrap(data.value);
// Không có dữ liệu avatar → trả false để ẩn HẲN ô (trả undefined sẽ bị render thành icon rỗng).
if (!staffInfo) return of(false as unknown as IAvatarConfig);
// link_avatar rỗng/sai → avatar tự fallback sang chữ initials nhờ idGenColor + textAvatar.
return of({
linkAvatar: (staffInfo as { link_avatar?: string }).link_avatar,
idGenColor: (staffInfo as { username?: string }).username,
textAvatar: (staffInfo as { name?: string }).name,
size: 32,
});
},
}],
}],
},
{
label: 'Name',
orderby: 'name',
hasSort: true,
ngClass: { 'w-1/5': true },
colTemplateConfig: [{ cssWrapper: 'font-semibold', fieldsConfig: [{ field: 'name' }] }],
},
];
readonly userRequestConfig = signal<IHttpRequestConfig>({
objectInstance: new MockUserTableService(),
functionName: 'list',
argumentsValue: [new HttpParams()],
});
handlerSort(event: ISortEvent): void {
event.stopPropagation();
console.log('Sort changed:', event.sort);
}2. Click Pagination
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerRight]="headerStandard"
[httpRequestData]="userRequestConfigPagination()"
[activityLoadData]="'click-pagination'"
[paginationSetting]="{
showTotalPage: true,
showInputGotoPage: true,
numberPageDisplay: 5
}"
(outSort)="handlerSort($event)"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig } from '@libs-ui/components-table';
import { signal } from '@angular/core';
import { HttpParams } from '@angular/common/http';
readonly userRequestConfigPagination = signal({
objectInstance: new MockUserTableService(),
functionName: 'list',
argumentsValue: [new HttpParams()],
});
// MockUserTableService.list() nhận params { page, pageSize }
// và phải trả về: { data: [...], paging: { page, pageSize, total_items, total_pages } }3. Pinned Columns & Footer
Ghim cột bên trái, hiển thị Footer tổng hợp với API riêng.
<libs_ui-components-table
[headerLeft]="headerLeft"
[headerRight]="headerRight"
[customPositionFooter]="'top'"
[footerLeft]="footerLeft"
[footerRight]="footerRight"
[showFooter]="true"
[httpRequestDataFooter]="httpRequestDataFooter"
[newData]="data"
[barButtons]="barButtons"
(outClickBarButton)="handlerClickBarButton($event)"
/>import {
LibsUiComponentsTableComponent,
ITableHeaderConfig,
ITableFooterConfig,
IButtonBarEvent,
TYPE_NEW_DATA_TABLE,
} from '@libs-ui/components-table';
import { IButton } from '@libs-ui/components-buttons-button';
import { IHttpRequestConfig } from '@libs-ui/services-http-request';
import { convertObjectToSignal } from '@libs-ui/utils';
headerLeft: Array<ITableHeaderConfig> = HeaderLeft();
headerRight: Array<ITableHeaderConfig> = headerRight();
footerLeft: Array<ITableFooterConfig> = FooterLeft();
footerRight: Array<ITableFooterConfig> = FooterRight();
data: TYPE_NEW_DATA_TABLE = { data: convertObjectToSignal(DATA()) };
barButtons: Array<IButton> = BarButtons();
httpRequestDataFooter: IHttpRequestConfig = {
objectInstance: ItemFooter(),
functionName: 'footerData',
argumentsValue: [],
};
handlerClickBarButton(event: IButtonBarEvent): void {
event.stopPropagation();
console.log('Keys selected:', event.keys);
event.hiddenBarButtons();
}4. Advanced Layout (Custom Header Height + Footer Position)
<libs_ui-components-table
[headerLeft]="headerLeft"
classHeaderContainerInclude="!h-[80px]"
[headerRight]="headerRight"
[customPositionFooter]="'bottom'"
[footerLeft]="footerLeft"
[footerRight]="footerRight"
[showFooter]="true"
[httpRequestDataFooter]="httpRequestDataFooter"
[newData]="data"
[barButtons]="barButtons"
(outClickBarButton)="handlerClickBarButton($event)"
/>import { LibsUiComponentsTableComponent } from '@libs-ui/components-table';
// classHeaderContainerInclude="!h-[80px]" → tuỳ chỉnh chiều cao header
// customPositionFooter="'bottom'" → footer hiển thị phía dưới bảng5. Collapse / Expand Row (KPI Report)
<libs_ui-components-table
class="h-full block overflow-hidden"
style="display: block; width: 100%"
[headerLeft]="kpiHeadersLeft"
[headerRight]="kpiHeadersRight"
[newData]="kpiData"
[ignoreBar]="true"
[configTemplateItemCollapseExpand]="kpiExpandConfig"
[ignoreCalculatorWidthHeader]="{ headerRight: true }"
/>import {
LibsUiComponentsTableComponent,
ITableHeaderConfig,
IConfigTemplateItemCollapseExpand,
TYPE_NEW_DATA_TABLE,
} from '@libs-ui/components-table';
import { convertObjectToSignal } from '@libs-ui/utils';
import { of } from 'rxjs';
kpiHeadersLeft: Array<ITableHeaderConfig> = KpiReportHeadersConfig(toggleExpand).slice(0, 1);
kpiHeadersRight: Array<ITableHeaderConfig> = KpiReportHeadersConfig(toggleExpand).slice(1);
kpiExpandConfig: IConfigTemplateItemCollapseExpand = {
fieldGetDataExpand: 'expand_data',
colTemplateConfig: [{
cssWrapper: 'flex w-full',
fieldsConfig: [{
field: 'children',
parseValue: (data) => {
const rows = data.value() || [];
const html = rows.map((r: any) => `<div class="p-2">${r.name}</div>`).join('');
return of(`<div class="bg-gray-50 p-4">${html}</div>`);
},
}],
}],
};
kpiData: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal([
{ id: 1, name: 'KPI 1', specificExpand: false, expand_data: [{ children: [{ name: 'Sub KPI A' }] }] },
]),
};
toggleExpand(item: any): void {
item.update((val: any) => ({ ...val, specificExpand: !val.specificExpand }));
}⚠️ Collapse/Expand: Mỗi item trong
dataphải có trườngspecificExpand: booleanđể điều hướng state ẩn/hiện. Trường dữ liệu mở rộng được khai báo quafieldGetDataExpand.
6. Profile List (Avatar + Badge + Chips + Context Menu)
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerLeft]="profileListHeaderLeft"
[headerRight]="profileListHeaderRight"
[newData]="profileListData"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
import { convertObjectToSignal } from '@libs-ui/utils';
readonly profileListHeaderLeft: Array<ITableHeaderConfig> = profileListHeaderLeft((data) => this.profileModal.set(data));
readonly profileListHeaderRight: Array<ITableHeaderConfig> = profileListHeaderRight();
readonly profileListData: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal(profileListMockData()),
};
// profileListMockData() trả về mảng profile có: fullName, source, phone, identifier,
// altEmails, email, gender, isAi (badge), tags (chip list)7. XSS Escaped Content
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerRight]="xssEscapedHeader"
[newData]="xssEscapedData"
[ignoreBar]="true"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
import { escapeHtml } from '@libs-ui/utils';
import { convertObjectToSignal } from '@libs-ui/utils';
import { of } from 'rxjs';
readonly xssEscapedHeader: Array<ITableHeaderConfig> = [
{
label: 'Input người dùng',
colTemplateConfig: [{
cssWrapper: '',
fieldsConfig: [{
field: 'rawInput',
parseValue: (data) => {
const escaped = escapeHtml(data.value as string);
return of(
`<span style="color:#166534;background:#f0fdf4;padding:3px 10px;
border-radius:4px;font-size:11px;font-family:monospace;
border:1px solid #86efac;">${escaped}</span>`
);
},
}],
}],
},
];
readonly xssEscapedData: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal([
{ id: '1', rawInput: '<script>alert("XSS")</script>' },
{ id: '2', rawInput: '<img src=x onerror=alert(1)>' },
]),
};⚠️ Bảo mật: Luôn dùng
escapeHtml()từ@libs-ui/utilstrước khi nhúng nội dung người dùng vàoparseValuetrả về HTML.
8. Performance Stress (1000 cột × 30 dòng)
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerRight]="headerPerformanceStress"
[newData]="dataPerformanceStress"
[ignoreCalculatorWidthHeader]="{ headerRight: true }"
[optimizeTableRenderByOnViewport]="true"
[useScrollMeasureColumn]="true"
[functionGetWidthItem]="handlerGetWidthItem"
[activityLoadData]="'click-pagination'"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
import { convertObjectToSignal } from '@libs-ui/utils';
import { TYPE_OBJECT } from '@libs-ui/interfaces-types';
readonly COL_COUNT = 1000;
readonly ROW_COUNT = 30;
public handlerGetWidthItem = async () => 64; // px — phải khớp min-w / max-w trong ngClass header
readonly headerPerformanceStress: Array<ITableHeaderConfig> = Array.from(
{ length: this.COL_COUNT },
(_, i) => ({
label: `C${i + 1}`,
ngClass: { 'shrink-0 min-w-[64px] max-w-[64px]': true },
colTemplateConfig: [{
cssWrapper: 'text-xs font-mono truncate max-w-[64px]',
fieldsConfig: [{ field: `f${i}` }],
}],
})
);
readonly dataPerformanceStress: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal(
Array.from({ length: this.ROW_COUNT }, (_, row) => {
const obj: TYPE_OBJECT = { id: row + 1 };
for (let c = 0; c < this.COL_COUNT; c++) obj[`f${c}`] = `${row + 1}-${c + 1}`;
return obj;
})
),
};⚠️ Performance: Khi dùng
useScrollMeasureColumn, bắt buộc truyềnfunctionGetWidthItemtrả về kích thước cột (px). Kết hợp[ignoreCalculatorWidthHeader]="{ headerRight: true }"để bỏ tính toán chiều rộng tự động.
9. Pinned Columns + Virtual Cột Ngang (useScrollMeasureColumn)
<libs_ui-components-table
class="h-full block"
style="display: block; height: 100%"
[headerLeft]="headerLeftPinnedScroll"
[headerRight]="headerRightPinnedScroll"
[newData]="dataPinnedScroll"
[ignoreCalculatorWidthHeader]="{ headerRight: true }"
[optimizeTableRenderByOnViewport]="true"
[useScrollMeasureColumn]="true"
[functionGetWidthItem]="handlerGetWidthItem"
[activityLoadData]="'click-pagination'"
/>import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
import { convertObjectToSignal } from '@libs-ui/utils';
import { TYPE_OBJECT } from '@libs-ui/interfaces-types';
readonly RIGHT_COL_COUNT = 3000;
readonly ROW_COUNT = 50;
public handlerGetWidthItem = async () => 64;
readonly headerLeftPinnedScroll: Array<ITableHeaderConfig> = [
{
label: 'ID',
ngClass: { 'shrink-0 min-w-[80px] max-w-[80px]': true },
colTemplateConfig: [{ cssWrapper: 'text-xs font-mono text-gray-500 truncate max-w-[80px]', fieldsConfig: [{ field: 'id' }] }],
},
{
label: 'Name',
ngClass: { 'shrink-0 min-w-[160px] max-w-[160px]': true },
colTemplateConfig: [{ cssWrapper: 'text-xs font-semibold truncate max-w-[160px]', fieldsConfig: [{ field: 'name' }] }],
},
];
readonly headerRightPinnedScroll: Array<ITableHeaderConfig> = Array.from(
{ length: this.RIGHT_COL_COUNT },
(_, i) => ({
label: `M${i + 1}`,
ngClass: { 'shrink-0 min-w-[64px] max-w-[64px]': true },
colTemplateConfig: [{ cssWrapper: 'text-xs font-mono truncate max-w-[64px]', fieldsConfig: [{ field: `m${i}` }] }],
})
);
readonly dataPinnedScroll: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal(
Array.from({ length: this.ROW_COUNT }, (_, row) => {
const obj: TYPE_OBJECT = { id: row + 1, name: `User ${row + 1}` };
for (let c = 0; c < this.RIGHT_COL_COUNT; c++) obj[`m${c}`] = `${row + 1}-${c + 1}`;
return obj;
})
),
};API Documentation
@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [httpRequestData] | IHttpRequestConfig | undefined | (Khuyên dùng) Cấu hình service để component tự gọi API, phân trang, sort. | [httpRequestData]="requestConfig" |
| [httpRequestDataFooter] | IHttpRequestConfig | undefined | Cấu hình service gọi API riêng cho Footer. | [httpRequestDataFooter]="footerConfig" |
| [headerLeft] | Array<ITableHeaderConfig> | [] | Cột ghim cố định bên trái (Sticky). | [headerLeft]="pinnedCols" |
| [headerRight] | Array<ITableHeaderConfig> | [] | Cột chính hiển thị trong vùng cuộn. | [headerRight]="mainCols" |
| [footerLeft] | Array<ITableFooterConfig> | undefined | Cấu hình footer phần ghim trái. | [footerLeft]="footerLeftCols" |
| [footerRight] | Array<ITableFooterConfig> | undefined | Cấu hình footer phần cuộn. | [footerRight]="footerRightCols" |
| [showFooter] | boolean | undefined | Hiển thị footer của bảng. | [showFooter]="true" |
| [customPositionFooter] | 'top' \| 'bottom' | 'bottom' | Vị trí footer (trên hoặc dưới bảng). | [customPositionFooter]="'top'" |
| [ignoreBorderFooter] | boolean | undefined | Ẩn border của footer. | [ignoreBorderFooter]="true" |
| [newData] | TYPE_NEW_DATA_TABLE | undefined | Dữ liệu tĩnh đẩy thẳng vào bảng (không dùng httpRequestData). | [newData]="{ data: convertObjectToSignal(list) }" |
| [filter] | TYPE_DATA_FILTER_TABLE | { filterData: {} } | Filter/search. Thay đổi filterData để trigger gọi API lại. | [filter]="{ filterData: { keySearch: 'abc' } }" |
| [activityLoadData] | 'scroll-infinity' \| 'click-pagination' | 'scroll-infinity' | Chế độ tải trang: cuộn vô hạn hoặc click phân trang. | [activityLoadData]="'click-pagination'" |
| [paginationSetting] | IPaginationConfig | undefined | Tuỳ chỉnh giao diện phân trang (số trang, ô nhảy trang, ...). | [paginationSetting]="{ showTotalPage: true }" |
| [fieldKey] | string | 'id' | Trường định danh duy nhất mỗi hàng. | [fieldKey]="'userId'" |
| [barButtons] | Array<IButton> | undefined | Nút thao tác xuất hiện trên bar khi có row được chọn. | [barButtons]="actionButtons" |
| [configTemplateItemCollapseExpand] | IConfigTemplateItemCollapseExpand | undefined | Cấu hình template Collapse/Expand row. | [configTemplateItemCollapseExpand]="expandConfig" |
| [noDataConfig] | ITableNoDataConfig | {textNoData: 'i18n_no_data_yet', textSearchNoData: 'i18n_no_result'} | Cấu hình text/icon khi không có dữ liệu. | [noDataConfig]="{ textNoData: 'Chưa có dữ liệu' }" |
| [useScrollMeasureColumn] | boolean | undefined | Bật Virtual Scroll cột ngang (ảo hoá render cột). | [useScrollMeasureColumn]="true" |
| [functionGetWidthItem] | () => Promise<number> | undefined | (Bắt buộc khi useScrollMeasureColumn=true) Hàm trả về px mỗi cột. | [functionGetWidthItem]="getColWidth" |
| [ignoreCalculatorWidthHeader] | { headerLeft?: boolean; headerRight?: boolean } | undefined | Bỏ qua tính toán chiều rộng tự động (dùng khi nhiều cột). | [ignoreCalculatorWidthHeader]="{ headerRight: true }" |
| [optimizeTableRenderByOnViewport] | boolean | undefined | Tối ưu render cell khi nằm trong viewport. | [optimizeTableRenderByOnViewport]="true" |
| [bufferAmount] | number | 5 | Số item buffer cho Virtual Scroller (tối đa 25). | [bufferAmount]="10" |
| [enableUnequalChildrenSizes] | boolean | false | Hỗ trợ item có kích thước không đều (Virtual Scroller). | [enableUnequalChildrenSizes]="true" |
| [totalItem] | number | undefined | Tổng số item (override tính toán từ paging). | [totalItem]="500" |
| [defaultKeysSelected] | Array<any> | undefined | Danh sách key được chọn mặc định khi khởi tạo. | [defaultKeysSelected]="['1','2']" |
| [maxItemSelected] | number | 100 | Số lượng item tối đa được chọn. | [maxItemSelected]="50" |
| [autoEmitKeysSelectedWithItems] | boolean | undefined | Khi true, outKeysSelected emit cả items (không chỉ keys). | [autoEmitKeysSelectedWithItems]="true" |
| [configSelectMoreItem] | IConfigSelectMoreItem | undefined | Dropdown chọn số lượng item từ droplist (tương tự "Chọn X bản ghi"). | [configSelectMoreItem]="moreItemConfig" |
| [disableCheckbox] | boolean | false | Vô hiệu hoá tất cả checkbox. | [disableCheckbox]="isReadOnly()" |
| [sortLocal] | boolean | undefined | Sort tại client, không gọi API. | [sortLocal]="true" |
| [filterOrSortLocal] | TYPE_TABLE_FILTER | undefined | Hàm filter/sort local tuỳ chỉnh. | [filterOrSortLocal]="customFilterFn" |
| [ignoreBar] | boolean | undefined | Ẩn toàn bộ bar (kể cả thanh đếm). | [ignoreBar]="true" |
| [ignoreClassBgHeader] | boolean | undefined | Bỏ class bg-white mặc định của header. | [ignoreClassBgHeader]="true" |
| [isDashBorder] | boolean | undefined | Đường viền dạng nét đứt. | [isDashBorder]="true" |
| [ignoreBorderItem] | boolean | undefined | Ẩn border giữa các row. | [ignoreBorderItem]="true" |
| [ignoreBorderItemLast] | boolean | undefined | Ẩn border của row cuối. | [ignoreBorderItemLast]="true" |
| [isHiddenHeaderWhenNodata] | boolean | undefined | Ẩn header khi không có dữ liệu. | [isHiddenHeaderWhenNodata]="true" |
| [showScrollTablePinnedIfNoData] | boolean | undefined | Vẫn hiển thị phần cuộn khi không có data. | [showScrollTablePinnedIfNoData]="true" |
| [onlyShowNoResult] | boolean | undefined | Chỉ hiển thị trạng thái "không có kết quả" (không hiển thị "không có dữ liệu"). | [onlyShowNoResult]="true" |
| [templateNoData] | TemplateRef | undefined | Custom template khi không có dữ liệu. | [templateNoData]="noDataTpl" |
| [templateNoResult] | TemplateRef | undefined | Custom template khi không có kết quả tìm kiếm. | [templateNoResult]="noResultTpl" |
| [timeHighlighNewItem] | number | 2000 | Thời gian (ms) highlight item mới thêm vào. | [timeHighlighNewItem]="3000" |
| [labelBarNoSelectItem] | string | 'i18n_total_quantity' | Label hiển thị khi không có item nào được chọn. | [labelBarNoSelectItem]="'Tổng số'" |
| [labelBarButtons] | string | 'i18n_number_item_selected' | Label số item đã chọn trên bar. | [labelBarButtons]="'i18n_selected'" |
| [classHeaderContainerInclude] | string | undefined | Class thêm vào container header. | classHeaderContainerInclude="!h-[80px]" |
| [classHeaderLeftInclude] | string | undefined | Class thêm vào header left. | classHeaderLeftInclude="bg-blue-50" |
| [classBodyInclude] | string | '' | Class thêm vào vùng body. | classBodyInclude="max-h-[400px]" |
| [classFooterInclude] | string | '' | Class thêm vào footer. | classFooterInclude="font-bold" |
| [classTableContainerInclude] | string | '' | Class thêm vào container bảng. | classTableContainerInclude="rounded-lg" |
| [classBarInclude] | string | 'justify-between mb-[16px]' | Class thêm vào bar. | classBarInclude="justify-end" |
| [classLabelBarButtons] | string | '' | Class cho label số item đã chọn. | classLabelBarButtons="text-blue-500" |
| [classLabelBarNoSelectItem] | string | undefined | Class cho label khi không chọn item. | classLabelBarNoSelectItem="text-gray-500" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outSort) | ISortEvent | Emit khi người dùng thay đổi sort cột. | handlerSort(e: ISortEvent): void { e.stopPropagation(); } | (outSort)="handlerSort($event)" |
| (outClickBarButton) | IButtonBarEvent | Emit khi click nút trên bar (có items chọn). | handlerBarButton(e: IButtonBarEvent): void { e.stopPropagation(); e.hiddenBarButtons(); } | (outClickBarButton)="handlerBarButton($event)" |
| (outKeysSelected) | { keys: Array<any>; items?: Array<WritableSignal<any>> } | Emit khi danh sách row được chọn thay đổi. | handlerKeysSelected(e: { keys: any[] }): void { e.stopPropagation(); } | (outKeysSelected)="handlerKeysSelected($event)" |
| (outLoadMore) | ILoadMoreEvent | Emit khi cần tải thêm dữ liệu (scroll infinity + không dùng httpRequestData). | handlerLoadMore(e: ILoadMoreEvent): void { e.stopPropagation(); } | (outLoadMore)="handlerLoadMore($event)" |
| (outLoading) | boolean | Emit trạng thái loading của component. | handlerLoading(loading: boolean): void { } | (outLoading)="handlerLoading($event)" |
| (outTotalItem) | number | Emit tổng số bản ghi từ API. | handlerTotalItem(total: number): void { this.total.set(total); } | (outTotalItem)="handlerTotalItem($event)" |
| (outFunctionsControl) | ITableFunctionControlEvent | Emit object chứa các hàm điều khiển bảng từ bên ngoài. | handlerFunctionsControl(ctrl: ITableFunctionControlEvent): void { this.tableCtrl = ctrl; } | (outFunctionsControl)="handlerFunctionsControl($event)" |
| (outLoadDataComplete) | WritableSignal<Array<WritableSignal<any>>> | Emit sau khi gọi API thành công, trả về signal chứa toàn bộ store. | handlerLoadComplete(store: any): void { } | (outLoadDataComplete)="handlerLoadComplete($event)" |
| (outLoadDataError) | IHttpResponseError | Emit khi gọi API thất bại. | handlerLoadError(err: IHttpResponseError): void { } | (outLoadDataError)="handlerLoadError($event)" |
| (outScrollIsGone) | ILoadMoreEvent | Emit khi scroll biến mất (dữ liệu ít hơn chiều cao container). | handlerScrollGone(e: ILoadMoreEvent): void { } | (outScrollIsGone)="handlerScrollGone($event)" |
FunctionsControl API
Nhận về qua (outFunctionsControl) để điều khiển bảng từ component cha.
import { ITableFunctionControlEvent } from '@libs-ui/components-table';
private tableCtrl?: ITableFunctionControlEvent;
handlerFunctionsControl(ctrl: ITableFunctionControlEvent): void {
this.tableCtrl = ctrl;
}
// Ví dụ sử dụng:
refreshTable(): void {
this.tableCtrl?.callApiByService(true); // Tải lại từ trang 1
}
resetTable(): void {
this.tableCtrl?.reset(true); // Reset + xóa sort
}
addItemToTop(newItem: any): void {
this.tableCtrl?.addItems(
convertObjectToSignal([newItem]),
true, // addFirst = true
);
}
removeItem(id: string): void {
this.tableCtrl?.removeItems([id]);
}
clearSelection(): void {
this.tableCtrl?.resetKeySelected();
}
getCount(): number {
return this.tableCtrl?.getCount() ?? 0;
}
jumpToPage(page: number): void {
this.tableCtrl?.changePagination(page);
}| Hàm | Signature | Mô tả |
|---|---|---|
| reset | (clearSort?: boolean) => void | Reset toàn bộ bảng về trạng thái ban đầu. |
| callApiByService | (firstCall: boolean, resetAfterCallApi?: boolean) => void | Gọi lại API. firstCall=true → tải từ trang 1. |
| resetScroll | (left?: number, top?: number) => void | Reset vị trí scroll. |
| addItems | (items: TYPE_DATA_TABLE, addFirst: boolean, ...) => void | Thêm items vào bảng. |
| removeItems | (keys: Array<unknown>, ...) => void | Xóa items theo key. |
| resetKeySelected | () => void | Xóa toàn bộ selection. |
| getCount | () => number | Lấy tổng số item hiện tại. |
| getStore | () => Array<WritableSignal<TYPE_OBJECT>> | Lấy toàn bộ store (kể cả item đã scroll qua). |
| getItems | () => Array<WritableSignal<TYPE_OBJECT>> | Lấy items đang hiển thị. |
| hightLightItem | (items: Array<TYPE_ITEM_DATA_TABLE>) => Promise<void> | Highlight item (hiệu ứng màu). |
| changePagination | (page?: number) => Promise<void> | Nhảy tới trang cụ thể. |
| setCountPagingStore | (count: number) => void | Cập nhật tổng số item trong paging store. |
| detectChanges | () => void | Trigger change detection thủ công. |
Types & Interfaces
ITableHeaderConfig
import { ITableHeaderConfig } from '@libs-ui/components-table';
export interface ITableHeaderConfig {
label?: string | TYPE_OBJECT; // Label header (hỗ trợ i18n object)
hasSort?: boolean; // Hiện nút sort
orderby?: string; // Field dùng để sort
hasCheckbox?: boolean; // Hiện checkbox
hasCheckboxAll?: boolean; // Hiện checkbox "Chọn tất cả"
disableCheckbox?: boolean; // Disable checkbox
colTemplateConfig?: Array<ITableTemplateConfig>; // Cấu hình hiển thị cell
rowsConfig?: { // Multi-row header
classContainerRows?: string;
rows: Array<{ classRow?: string; cols: Array<ITableHeaderConfigCol> }>;
};
ngStyle?: TYPE_OBJECT;
ngClass?: TYPE_OBJECT;
checkConditionCheckBoxHidden?: TYPE_FUNCTION<boolean>;
isShowIndexOnRow?: boolean; // Hiện số thứ tự dòng
defaultMode?: TYPE_SORT_TYPE; // Mode sort mặc định ('asc' | 'desc')
labelDescription?: { content: string; classInclude?: string };
tooltipsLeft?: Array<IPopover>;
tooltipsRight?: Array<IPopover>;
}ITableTemplateConfig & ITableFieldTemplateConfig
import { ITableTemplateConfig, ITableFieldTemplateConfig } from '@libs-ui/components-table';
export interface ITableTemplateConfig {
cssWrapper: string; // Class CSS cho wrapper của cell
fieldsConfig: Array<ITableFieldTemplateConfig>;
}
export interface ITableFieldTemplateConfig {
field: string; // Tên field trong data object
instance?: 'tooltip' | 'line-clamp' | 'buttons' | 'button-status'
| 'switch' | 'badge' | 'other-action-show-popup'
| 'shape-style' | 'image' | 'avatar' | 'button-action-show-popup';
parseValue?: TYPE_FUNCTION; // Custom render → trả về Observable<string> (HTML)
action?: (dataField, item, itemSelected?) => any;
getAvatarConfig?: TYPE_FUNCTION<IAvatarConfig | undefined>;
buttons?: Array<WritableSignal<IButton>>; // instance = 'buttons'
getButtonsByItem?: TYPE_FUNCTION<Array<WritableSignal<IButton>>>;
showButtonHoverMode?: boolean; // Chỉ hiện button khi hover
switchAction?: (switchData: ISwitchEvent, item) => void; // instance = 'switch'
getDisableValueSwitch?: TYPE_FUNCTION;
getActiveValueSwitch?: TYPE_FUNCTION;
lineClampConfig?: ILineClampConfig; // instance = 'line-clamp'
ngStyle?: TYPE_OBJECT;
getNgStyle?: TYPE_FUNCTION;
ngClass?: TYPE_OBJECT;
getNgClass?: TYPE_FUNCTION;
rows?: Array<ITableTemplateConfig>; // Sub-rows trong cell
getComponentOutlet?: () => Observable<any>;
getDataComponentOutlet?: TYPE_FUNCTION;
}IConfigTemplateItemCollapseExpand
import { IConfigTemplateItemCollapseExpand } from '@libs-ui/components-table';
export interface IConfigTemplateItemCollapseExpand {
fieldGetDataExpand?: string; // Trường chứa dữ liệu con trong mỗi row
cssWrapper?: string;
templateCssWrapper?: string;
templateCssWrapperHost?: string;
colTemplateConfig?: Array<ITableTemplateConfig>;
}
// Mỗi item trong data cần có: specificExpand: boolean (quản lý state đóng/mở)TYPE_NEW_DATA_TABLE
import { TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
// Cách tạo dữ liệu tĩnh:
import { convertObjectToSignal } from '@libs-ui/utils';
const data: TYPE_NEW_DATA_TABLE = {
data: convertObjectToSignal([
{ id: '1', name: 'Item 1' },
{ id: '2', name: 'Item 2' },
]),
addToLastList: false, // true = thêm xuống cuối, false = reset và set mới
};IHttpRequestConfig (httpRequestData)
import { IHttpRequestConfig } from '@libs-ui/services-http-request';
// Service phải có method trả về Observable với cấu trúc:
// { data: Array<any>, paging?: { page, pageSize, total_items, total_pages } }
const requestConfig: IHttpRequestConfig = {
objectInstance: myService, // Instance của service (inject qua inject())
functionName: 'getList', // Tên method trong service
argumentsValue: [new HttpParams()], // Mảng tham số truyền vào method
};Lưu ý quan trọng
⚠️ Container cần chiều cao xác định: Virtual Scroller cần biết chiều cao để tính số item render. Container bao ngoài phải có chiều cao cố định (VD: <div class="h-[500px]">). Nếu muốn table fill 100% container, thêm class="h-full block" vào thẻ <libs_ui-components-table>, nhưng nếu container đã đủ cao thì không cần.
⚠️ useScrollMeasureColumn: Bắt buộc kèm functionGetWidthItem trả về px đúng với min-w / max-w trong ngClass header. Thiếu sẽ tính toán sai.
⚠️ Collapse/Expand: Item data phải có trường specificExpand: boolean. Toggle bằng cách item.update(val => ({ ...val, specificExpand: !val.specificExpand })).
⚠️ httpRequestData + filter: Khi filter().filterData thay đổi, component tự động gọi lại API từ trang 1. Dùng filter.ignoreCallApiAuto = true nếu muốn tự kiểm soát.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/table
License
MIT
