@libs-ui/components-table 0.2.356-41 → 0.2.356-43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,27 +1,30 @@
1
1
  # @libs-ui/components-table
2
2
 
3
- > Component hiển thị dữ liệu dạng bảng mạnh mẽ với khả năng ghim cột, Virtual Scrolling, phân trang và hỗ trợ gọi API trực tiếp qua `httpRequestData`.
3
+ > 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.
4
4
 
5
5
  ## Giới thiệu
6
6
 
7
- `LibsUiComponentsTableComponent` là một 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 hiệu năng, chế ghim cột (Fixed Columns) linh hoạtkhả năng kết nối trực tiếp với các API service thông qua cấu hình `httpRequestData`.
7
+ `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 chế kết nối trực tiếp với API service qua `httpRequestData`.
8
8
 
9
9
  ### Tính năng
10
10
 
11
- - ✅ **Ghim cột (Sticky Columns)**: Hỗ trợ ghim các cột quan trọng bên trái thông qua `headerLeft`.
12
- - ✅ **Virtual Scrolling**: Tối ưu hiệu năng cho danh sách hàng ngàn bản ghi.
13
- - ✅ **Phân trang (Pagination)**: Hỗ trợ cả `scroll-infinity` và `click-pagination`.
14
- - ✅ **Custom Template**: Tùy biến hiển thị từng row và cell thông qua cấu hình `colTemplateConfig`.
15
- - ✅ **Collapse/Expand Row**: Hỗ trợ mở rộng xem chi tiết dữ liệu cấp dưới (Hierarchical/Sub-table) bằng giao diện có thể tuỳ biến cao.
16
- - ✅ **Sorting**: Tích hợp sẵn logic sắp xếp dữ liệu.
17
- - ✅ **Checkbox Selection**: Hỗ trợ chọn một hoặc nhiều hàng cùng lúc.
18
- - ✅ **Angular Signals**: Hoạt động mượt với Signal-based state management.
11
+ - ✅ **Ghim cột (Sticky Columns)**: Cột quan trọng luôn hiển thị qua `headerLeft`.
12
+ - ✅ **Virtual Scrolling**: Tối ưu hiệu năng cho danh sách hàng nghìn bản ghi.
13
+ - ✅ **Phân trang**: Hỗ trợ `scroll-infinity` và `click-pagination`.
14
+ - ✅ **Virtual cột ngang**: Ảo hoá render cột ngang với `useScrollMeasureColumn`.
15
+ - ✅ **Collapse/Expand Row**: Mở rộng xem chi tiết dữ liệu cấp dưới.
16
+ - ✅ **Checkbox Selection**: Chọn một hoặc nhiều hàng.
17
+ - ✅ **Footer tổng hợp**: Footer thể gọi API riêng, đặt trên hoặc dưới.
18
+ - ✅ **Bar Buttons**: Thanh công cụ xuất hiện khi row được chọn.
19
+ - ✅ **Sorting**: Sort local hoặc server-side.
20
+ - ✅ **Angular Signals + OnPush**: Hiệu năng cao.
19
21
 
20
22
  ## Khi nào sử dụng
21
23
 
22
- - Khi cần hiển thị dữ liệu dạng bảng với số lượng bản ghi lớn.
23
- - Khi cần các tính năng Sort/Filter/Pagination kết nối trực tiếp với Backend.
24
- - Khi bảng nhiều cột thông tin cần giữ các cột quan trọng luôn hiển thị (ghim cột).
24
+ - Hiển thị danh sách dữ liệu lớn cần Virtual Scroller.
25
+ - Bảng nhiều cột cần ghim cột quan trọng (`headerLeft` / `headerRight`).
26
+ - Cần Footer tổng hợp, Bar Buttons, hoặc tính năng Collapse/Expand row.
27
+ - Kết nối trực tiếp API service qua `httpRequestData`.
25
28
 
26
29
  ## Cài đặt
27
30
 
@@ -37,179 +40,709 @@ import { LibsUiComponentsTableComponent } from '@libs-ui/components-table';
37
40
  @Component({
38
41
  standalone: true,
39
42
  imports: [LibsUiComponentsTableComponent],
40
- // ...
41
43
  })
42
44
  export class YourComponent {}
43
45
  ```
44
46
 
47
+ ---
48
+
45
49
  ## Ví dụ sử dụng
46
50
 
47
- ### 1. Table thường với httpRequestData
51
+ ### 1. Standard Table (Virtual Scrolling + httpRequestData)
48
52
 
49
- Sử dụng `httpRequestData` là cách tốt nhất để component tự động quản lý việc tải dữ liệu, phân trang và sắp xếp.
53
+ ```html
54
+ <libs_ui-components-table
55
+ class="h-full block"
56
+ style="display: block; height: 100%"
57
+ [headerRight]="headerStandard"
58
+ [httpRequestData]="userRequestConfig()"
59
+ (outSort)="handlerSort($event)"
60
+ />
61
+ ```
62
+
63
+ ```typescript
64
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig, ISortEvent } from '@libs-ui/components-table';
65
+ import { IHttpRequestConfig } from '@libs-ui/services-http-request';
66
+ import { IAvatarConfig } from '@libs-ui/components-avatar';
67
+ import { signal } from '@angular/core';
68
+ import { of } from 'rxjs';
69
+
70
+ readonly headerStandard: Array<ITableHeaderConfig> = [
71
+ {
72
+ label: 'ID',
73
+ orderby: 'id',
74
+ hasSort: true,
75
+ ngClass: { 'w-1/5': true },
76
+ colTemplateConfig: [{ cssWrapper: 'font-mono text-gray-400', fieldsConfig: [{ field: 'id' }] }],
77
+ },
78
+ {
79
+ label: 'Avatar',
80
+ ngClass: { 'w-1/5': true },
81
+ colTemplateConfig: [{
82
+ cssWrapper: 'justify-center',
83
+ fieldsConfig: [{
84
+ field: 'staff_info',
85
+ instance: 'avatar',
86
+ getAvatarConfig: (data) => {
87
+ if (!data.value()) return of(undefined as unknown as IAvatarConfig);
88
+ return of({
89
+ linkAvatar: data.value().link_avatar,
90
+ idGenColor: data.value().username,
91
+ textAvatar: data.value().name,
92
+ size: 32,
93
+ });
94
+ },
95
+ }],
96
+ }],
97
+ },
98
+ {
99
+ label: 'Name',
100
+ orderby: 'name',
101
+ hasSort: true,
102
+ ngClass: { 'w-1/5': true },
103
+ colTemplateConfig: [{ cssWrapper: 'font-semibold', fieldsConfig: [{ field: 'name' }] }],
104
+ },
105
+ ];
106
+
107
+ readonly userRequestConfig = signal<IHttpRequestConfig>({
108
+ objectInstance: new MockUserTableService(),
109
+ functionName: 'list',
110
+ argumentsValue: [new HttpParams()],
111
+ });
112
+
113
+ handlerSort(event: ISortEvent): void {
114
+ event.stopPropagation();
115
+ console.log('Sort changed:', event.sort);
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ### 2. Click Pagination
50
122
 
51
123
  ```html
52
124
  <libs_ui-components-table
53
- [headerRight]="headerConfig"
54
- [httpRequestData]="userRequestConfig" />
125
+ class="h-full block"
126
+ style="display: block; height: 100%"
127
+ [headerRight]="headerStandard"
128
+ [httpRequestData]="userRequestConfigPagination()"
129
+ [activityLoadData]="'click-pagination'"
130
+ [paginationSetting]="{
131
+ showTotalPage: true,
132
+ showInputGotoPage: true,
133
+ numberPageDisplay: 5
134
+ }"
135
+ (outSort)="handlerSort($event)"
136
+ />
55
137
  ```
56
138
 
57
139
  ```typescript
58
- // Cấu hình gọi API từ service
59
- readonly userRequestConfig = {
60
- objectInstance: userService, // Inject của service chứa method lấy data
61
- functionName: 'getUsers', // Tên phương thức gọi API
62
- argumentsValue: [] // Tham số bổ sung nếu có
140
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig } from '@libs-ui/components-table';
141
+ import { signal } from '@angular/core';
142
+ import { HttpParams } from '@angular/common/http';
143
+
144
+ readonly userRequestConfigPagination = signal({
145
+ objectInstance: new MockUserTableService(),
146
+ functionName: 'list',
147
+ argumentsValue: [new HttpParams()],
148
+ });
149
+ // MockUserTableService.list() nhận params { page, pageSize }
150
+ // và phải trả về: { data: [...], paging: { page, pageSize, total_items, total_pages } }
151
+ ```
152
+
153
+ ---
154
+
155
+ ### 3. Pinned Columns & Footer
156
+
157
+ Ghim cột bên trái, hiển thị Footer tổng hợp với API riêng.
158
+
159
+ ```html
160
+ <libs_ui-components-table
161
+ [headerLeft]="headerLeft"
162
+ [headerRight]="headerRight"
163
+ [customPositionFooter]="'top'"
164
+ [footerLeft]="footerLeft"
165
+ [footerRight]="footerRight"
166
+ [showFooter]="true"
167
+ [httpRequestDataFooter]="httpRequestDataFooter"
168
+ [newData]="data"
169
+ [barButtons]="barButtons"
170
+ (outClickBarButton)="handlerClickBarButton($event)"
171
+ />
172
+ ```
173
+
174
+ ```typescript
175
+ import {
176
+ LibsUiComponentsTableComponent,
177
+ ITableHeaderConfig,
178
+ ITableFooterConfig,
179
+ IButtonBarEvent,
180
+ TYPE_NEW_DATA_TABLE,
181
+ } from '@libs-ui/components-table';
182
+ import { IButton } from '@libs-ui/components-buttons-button';
183
+ import { IHttpRequestConfig } from '@libs-ui/services-http-request';
184
+ import { convertObjectToSignal } from '@libs-ui/utils';
185
+
186
+ headerLeft: Array<ITableHeaderConfig> = HeaderLeft();
187
+ headerRight: Array<ITableHeaderConfig> = headerRight();
188
+ footerLeft: Array<ITableFooterConfig> = FooterLeft();
189
+ footerRight: Array<ITableFooterConfig> = FooterRight();
190
+ data: TYPE_NEW_DATA_TABLE = { data: convertObjectToSignal(DATA()) };
191
+ barButtons: Array<IButton> = BarButtons();
192
+ httpRequestDataFooter: IHttpRequestConfig = {
193
+ objectInstance: ItemFooter(),
194
+ functionName: 'footerData',
195
+ argumentsValue: [],
63
196
  };
64
197
 
65
- readonly headerConfig = [
66
- { label: 'ID', colTemplateConfig: [{ cssWrapper: '', fieldsConfig: [{ field: 'id' }] }] },
67
- { label: 'Name', hasSort: true, orderby: 'name', colTemplateConfig: [{ cssWrapper: 'font-bold', fieldsConfig: [{ field: 'name' }] }] }
68
- ];
198
+ handlerClickBarButton(event: IButtonBarEvent): void {
199
+ event.stopPropagation();
200
+ console.log('Keys selected:', event.keys);
201
+ event.hiddenBarButtons();
202
+ }
69
203
  ```
70
204
 
71
- ### 2. Table ghim cột (Fixed Columns)
205
+ ---
72
206
 
73
- Ghim các cột quan trọng bên trái bằng cách sử dụng `headerLeft`. Các cột trong `headerRight` sẽ có thể cuộn ngang nếu vượt quá chiều rộng container.
207
+ ### 4. Advanced Layout (Custom Header Height + Footer Position)
74
208
 
75
209
  ```html
76
210
  <libs_ui-components-table
77
- [headerLeft]="leftPinnedColumns"
78
- [headerRight]="scrollableColumns"
79
- [httpRequestData]="userRequestConfig" />
211
+ [headerLeft]="headerLeft"
212
+ classHeaderContainerInclude="!h-[80px]"
213
+ [headerRight]="headerRight"
214
+ [customPositionFooter]="'bottom'"
215
+ [footerLeft]="footerLeft"
216
+ [footerRight]="footerRight"
217
+ [showFooter]="true"
218
+ [httpRequestDataFooter]="httpRequestDataFooter"
219
+ [newData]="data"
220
+ [barButtons]="barButtons"
221
+ (outClickBarButton)="handlerClickBarButton($event)"
222
+ />
80
223
  ```
81
224
 
82
- ### 3. Hierarchical Data (Collapse/Expand Row)
225
+ ```typescript
226
+ import { LibsUiComponentsTableComponent } from '@libs-ui/components-table';
227
+ // classHeaderContainerInclude="!h-[80px]" → tuỳ chỉnh chiều cao header
228
+ // customPositionFooter="'bottom'" → footer hiển thị phía dưới bảng
229
+ ```
230
+
231
+ ---
83
232
 
84
- Sử dụng `configTemplateItemCollapseExpand` để cấu hình hiển thị nội dung chi tiết mở rộng bên dưới mỗi hàng. Rất hữu ích cho các Sub-table (bảng con), Report nhiều lớp, v.v.
233
+ ### 5. Collapse / Expand Row (KPI Report)
85
234
 
86
235
  ```html
87
236
  <libs_ui-components-table
88
- [headerRight]="headerConfig"
89
- [newData]="tableData"
90
- [configTemplateItemCollapseExpand]="configCollapseExpand" />
91
- ```
92
-
93
- ```typescript
94
- // Cấu hình Collapse/Expand Template
95
- readonly configCollapseExpand: IConfigTemplateItemCollapseExpand = {
96
- // Biến dùng để nhận diện nội dung mở rộng mảng object chứa dữ liệu
97
- fieldGetDataExpand: 'expand_data',
98
- colTemplateConfig: [
99
- {
100
- cssWrapper: 'flex w-full',
101
- fieldsConfig: [
102
- {
103
- field: 'details',
104
- parseValue: (data) => {
105
- // Un-wrap Signal và trả về HTML động tuỳ ý hoặc Component con
106
- const details = data.value() || [];
107
- let htmlInner = '';
108
- details.forEach(d => {
109
- htmlInner += `<div>Row Child Detail: \${d.name}</div>`;
110
- });
111
- return of(\`<div class="p-4 bg-gray-50">\${htmlInner}</div>\`);
112
- }
113
- }
114
- ]
115
- }
116
- ]
237
+ class="h-full block overflow-hidden"
238
+ style="display: block; width: 100%"
239
+ [headerLeft]="kpiHeadersLeft"
240
+ [headerRight]="kpiHeadersRight"
241
+ [newData]="kpiData"
242
+ [ignoreBar]="true"
243
+ [configTemplateItemCollapseExpand]="kpiExpandConfig"
244
+ [ignoreCalculatorWidthHeader]="{ headerRight: true }"
245
+ />
246
+ ```
247
+
248
+ ```typescript
249
+ import {
250
+ LibsUiComponentsTableComponent,
251
+ ITableHeaderConfig,
252
+ IConfigTemplateItemCollapseExpand,
253
+ TYPE_NEW_DATA_TABLE,
254
+ } from '@libs-ui/components-table';
255
+ import { convertObjectToSignal } from '@libs-ui/utils';
256
+ import { of } from 'rxjs';
257
+
258
+ kpiHeadersLeft: Array<ITableHeaderConfig> = KpiReportHeadersConfig(toggleExpand).slice(0, 1);
259
+ kpiHeadersRight: Array<ITableHeaderConfig> = KpiReportHeadersConfig(toggleExpand).slice(1);
260
+ kpiExpandConfig: IConfigTemplateItemCollapseExpand = {
261
+ fieldGetDataExpand: 'expand_data',
262
+ colTemplateConfig: [{
263
+ cssWrapper: 'flex w-full',
264
+ fieldsConfig: [{
265
+ field: 'children',
266
+ parseValue: (data) => {
267
+ const rows = data.value() || [];
268
+ const html = rows.map((r: any) => `<div class="p-2">${r.name}</div>`).join('');
269
+ return of(`<div class="bg-gray-50 p-4">${html}</div>`);
270
+ },
271
+ }],
272
+ }],
117
273
  };
274
+ kpiData: TYPE_NEW_DATA_TABLE = {
275
+ data: convertObjectToSignal([
276
+ { id: 1, name: 'KPI 1', specificExpand: false, expand_data: [{ children: [{ name: 'Sub KPI A' }] }] },
277
+ ]),
278
+ };
279
+
280
+ toggleExpand(item: any): void {
281
+ item.update((val: any) => ({ ...val, specificExpand: !val.specificExpand }));
282
+ }
283
+ ```
284
+
285
+ > ⚠️ **Collapse/Expand**: Mỗi item trong `data` phải có trường `specificExpand: boolean` để điều hướng state ẩn/hiện. Trường dữ liệu mở rộng được khai báo qua `fieldGetDataExpand`.
286
+
287
+ ---
288
+
289
+ ### 6. Profile List (Avatar + Badge + Chips + Context Menu)
290
+
291
+ ```html
292
+ <libs_ui-components-table
293
+ class="h-full block"
294
+ style="display: block; height: 100%"
295
+ [headerLeft]="profileListHeaderLeft"
296
+ [headerRight]="profileListHeaderRight"
297
+ [newData]="profileListData"
298
+ />
299
+ ```
300
+
301
+ ```typescript
302
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
303
+ import { convertObjectToSignal } from '@libs-ui/utils';
304
+
305
+ readonly profileListHeaderLeft: Array<ITableHeaderConfig> = profileListHeaderLeft((data) => this.profileModal.set(data));
306
+ readonly profileListHeaderRight: Array<ITableHeaderConfig> = profileListHeaderRight();
307
+ readonly profileListData: TYPE_NEW_DATA_TABLE = {
308
+ data: convertObjectToSignal(profileListMockData()),
309
+ };
310
+ // profileListMockData() trả về mảng profile có: fullName, source, phone, identifier,
311
+ // altEmails, email, gender, isAi (badge), tags (chip list)
312
+ ```
118
313
 
119
- // Dữ liệu cần có cờ \`specificExpand\` để điều hướng state ẩn/hiện,
120
- // và trường thông tin \`expand_data\` như đã định nghĩa ở trên.
121
- readonly tableData = {
314
+ ---
315
+
316
+ ### 7. XSS Escaped Content
317
+
318
+ ```html
319
+ <libs_ui-components-table
320
+ class="h-full block"
321
+ style="display: block; height: 100%"
322
+ [headerRight]="xssEscapedHeader"
323
+ [newData]="xssEscapedData"
324
+ [ignoreBar]="true"
325
+ />
326
+ ```
327
+
328
+ ```typescript
329
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
330
+ import { escapeHtml } from '@libs-ui/utils';
331
+ import { convertObjectToSignal } from '@libs-ui/utils';
332
+ import { of } from 'rxjs';
333
+
334
+ readonly xssEscapedHeader: Array<ITableHeaderConfig> = [
335
+ {
336
+ label: 'Input người dùng',
337
+ colTemplateConfig: [{
338
+ cssWrapper: '',
339
+ fieldsConfig: [{
340
+ field: 'rawInput',
341
+ parseValue: (data) => {
342
+ const escaped = escapeHtml(data.value as string);
343
+ return of(
344
+ `<span style="color:#166534;background:#f0fdf4;padding:3px 10px;
345
+ border-radius:4px;font-size:11px;font-family:monospace;
346
+ border:1px solid #86efac;">${escaped}</span>`
347
+ );
348
+ },
349
+ }],
350
+ }],
351
+ },
352
+ ];
353
+
354
+ readonly xssEscapedData: TYPE_NEW_DATA_TABLE = {
122
355
  data: convertObjectToSignal([
123
- {
124
- id: 1,
125
- name: 'Task Parent 1',
126
- specificExpand: false, // Quản lý state đóng mở
127
- expand_data: [{ details: [ {name: 'Sub Task A'}, {name: 'Sub Task B'} ] }]
128
- }
129
- ])
356
+ { id: '1', rawInput: '<script>alert("XSS")</script>' },
357
+ { id: '2', rawInput: '<img src=x onerror=alert(1)>' },
358
+ ]),
359
+ };
360
+ ```
361
+
362
+ > ⚠️ **Bảo mật**: Luôn dùng `escapeHtml()` từ `@libs-ui/utils` trước khi nhúng nội dung người dùng vào `parseValue` trả về HTML.
363
+
364
+ ---
365
+
366
+ ### 8. Performance Stress (1000 cột × 30 dòng)
367
+
368
+ ```html
369
+ <libs_ui-components-table
370
+ class="h-full block"
371
+ style="display: block; height: 100%"
372
+ [headerRight]="headerPerformanceStress"
373
+ [newData]="dataPerformanceStress"
374
+ [ignoreCalculatorWidthHeader]="{ headerRight: true }"
375
+ [optimizeTableRenderByOnViewport]="true"
376
+ [useScrollMeasureColumn]="true"
377
+ [functionGetWidthItem]="handlerGetWidthItem"
378
+ [activityLoadData]="'click-pagination'"
379
+ />
380
+ ```
381
+
382
+ ```typescript
383
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
384
+ import { convertObjectToSignal } from '@libs-ui/utils';
385
+ import { TYPE_OBJECT } from '@libs-ui/interfaces-types';
386
+
387
+ readonly COL_COUNT = 1000;
388
+ readonly ROW_COUNT = 30;
389
+
390
+ public handlerGetWidthItem = async () => 64; // px — phải khớp min-w / max-w trong ngClass header
391
+
392
+ readonly headerPerformanceStress: Array<ITableHeaderConfig> = Array.from(
393
+ { length: this.COL_COUNT },
394
+ (_, i) => ({
395
+ label: `C${i + 1}`,
396
+ ngClass: { 'shrink-0 min-w-[64px] max-w-[64px]': true },
397
+ colTemplateConfig: [{
398
+ cssWrapper: 'text-xs font-mono truncate max-w-[64px]',
399
+ fieldsConfig: [{ field: `f${i}` }],
400
+ }],
401
+ })
402
+ );
403
+
404
+ readonly dataPerformanceStress: TYPE_NEW_DATA_TABLE = {
405
+ data: convertObjectToSignal(
406
+ Array.from({ length: this.ROW_COUNT }, (_, row) => {
407
+ const obj: TYPE_OBJECT = { id: row + 1 };
408
+ for (let c = 0; c < this.COL_COUNT; c++) obj[`f${c}`] = `${row + 1}-${c + 1}`;
409
+ return obj;
410
+ })
411
+ ),
412
+ };
413
+ ```
414
+
415
+ > ⚠️ **Performance**: Khi dùng `useScrollMeasureColumn`, bắt buộc truyền `functionGetWidthItem` trả 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.
416
+
417
+ ---
418
+
419
+ ### 9. Pinned Columns + Virtual Cột Ngang (useScrollMeasureColumn)
420
+
421
+ ```html
422
+ <libs_ui-components-table
423
+ class="h-full block"
424
+ style="display: block; height: 100%"
425
+ [headerLeft]="headerLeftPinnedScroll"
426
+ [headerRight]="headerRightPinnedScroll"
427
+ [newData]="dataPinnedScroll"
428
+ [ignoreCalculatorWidthHeader]="{ headerRight: true }"
429
+ [optimizeTableRenderByOnViewport]="true"
430
+ [useScrollMeasureColumn]="true"
431
+ [functionGetWidthItem]="handlerGetWidthItem"
432
+ [activityLoadData]="'click-pagination'"
433
+ />
434
+ ```
435
+
436
+ ```typescript
437
+ import { LibsUiComponentsTableComponent, ITableHeaderConfig, TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
438
+ import { convertObjectToSignal } from '@libs-ui/utils';
439
+ import { TYPE_OBJECT } from '@libs-ui/interfaces-types';
440
+
441
+ readonly RIGHT_COL_COUNT = 3000;
442
+ readonly ROW_COUNT = 50;
443
+
444
+ public handlerGetWidthItem = async () => 64;
445
+
446
+ readonly headerLeftPinnedScroll: Array<ITableHeaderConfig> = [
447
+ {
448
+ label: 'ID',
449
+ ngClass: { 'shrink-0 min-w-[80px] max-w-[80px]': true },
450
+ colTemplateConfig: [{ cssWrapper: 'text-xs font-mono text-gray-500 truncate max-w-[80px]', fieldsConfig: [{ field: 'id' }] }],
451
+ },
452
+ {
453
+ label: 'Name',
454
+ ngClass: { 'shrink-0 min-w-[160px] max-w-[160px]': true },
455
+ colTemplateConfig: [{ cssWrapper: 'text-xs font-semibold truncate max-w-[160px]', fieldsConfig: [{ field: 'name' }] }],
456
+ },
457
+ ];
458
+
459
+ readonly headerRightPinnedScroll: Array<ITableHeaderConfig> = Array.from(
460
+ { length: this.RIGHT_COL_COUNT },
461
+ (_, i) => ({
462
+ label: `M${i + 1}`,
463
+ ngClass: { 'shrink-0 min-w-[64px] max-w-[64px]': true },
464
+ colTemplateConfig: [{ cssWrapper: 'text-xs font-mono truncate max-w-[64px]', fieldsConfig: [{ field: `m${i}` }] }],
465
+ })
466
+ );
467
+
468
+ readonly dataPinnedScroll: TYPE_NEW_DATA_TABLE = {
469
+ data: convertObjectToSignal(
470
+ Array.from({ length: this.ROW_COUNT }, (_, row) => {
471
+ const obj: TYPE_OBJECT = { id: row + 1, name: `User ${row + 1}` };
472
+ for (let c = 0; c < this.RIGHT_COL_COUNT; c++) obj[`m${c}`] = `${row + 1}-${c + 1}`;
473
+ return obj;
474
+ })
475
+ ),
130
476
  };
131
477
  ```
132
478
 
479
+ ---
480
+
133
481
  ## API Documentation
134
482
 
135
- ### Inputs
136
-
137
- | Property | Type | Default | Description |
138
- | :----------------------------------- | :---------------------------------------- | :------------------- | :--------------------------------------------------------------------------- |
139
- | `[httpRequestData]` | `IHttpRequestConfig` | `undefined` | **(Khuyên dùng)** Cấu hình service để component tự động gọi API lấy dữ liệu. |
140
- | `[headerLeft]` | `ITableHeaderConfig[]` | `[]` | Danh sách cột được ghim cố định bên trái (Sticky). |
141
- | `[headerRight]` | `ITableHeaderConfig[]` | `[]` | Danh sách các cột chính hiển thị trong vùng cuộn. |
142
- | `[paginationSetting]` | `object` | `undefined` | Cấu hình hiển thị phân trang (showTotalPage, showInputGotoPage, ...). |
143
- | `[fieldKey]` | `string` | `'id'` | Thuộc tính định danh duy nhất cho mỗi hàng dữ liệu. |
144
- | `[loading]` | `boolean` | `false` | Trạng thái xoay loading (thường dùng khi không dùng httpRequestData). |
145
- | `[newData]` | `TYPE_NEW_DATA_TABLE` | `undefined` | Dữ liệu đẩy trực tiếp vào bảng (dùng khi không dùng httpRequestData). |
146
- | `[showFooter]` | `boolean` | `false` | Hiển thị footer của bảng. |
147
- | `[footerLeft]` | `ITableFooterConfig[]` | `[]` | Cấu hình footer ghim bên trái. |
148
- | `[footerRight]` | `ITableFooterConfig[]` | `[]` | Cấu hình footer bên vùng cuộn. |
149
- | `[filter]` | `TYPE_DATA_FILTER_TABLE` | `{ filterData: {} }` | Cấu hình filter/search dữ liệu. |
150
- | `[activityLoadData]` | `'scroll-infinity' \| 'click-pagination'` | `'scroll-infinity'` | Chế độ tải trang (vô hạn hoặc click). |
151
- | `[barButtons]` | `IButton[]` | `[]` | Danh sách các nút thao tác nhanh trên header bar. |
152
- | `[optimizeTableRenderByOnViewport]` | `boolean` | `false` | Tối ưu render bảng khi nằm trong viewport. |
153
- | `[bufferAmount]` | `number` | `5` | Số lượng item render buffer cho Virtual Scroller. |
154
- | `[enableUnequalChildrenSizes]` | `boolean` | `false` | Hỗ trợ item kích thước không đều trong Virtual Scroller. |
155
- | `[configTemplateItemCollapseExpand]` | `IConfigTemplateItemCollapseExpand` | `undefined` | Cấu hình template cho chức năng collapse/expand row. |
156
- | `[noDataConfig]` | `ITableNoDataConfig` | `{...}` | Cấu hình text/icon khi không dữ liệu. |
157
-
158
- #### Outputs
159
-
160
- | Property | Type | Description |
161
- | ------------------------ | ------------------------- | ---------------------------------------------------------- |
162
- | `(outKeysSelected)` | `Array<any>` | Emit danh sách các `fieldKey` của các hàng đang được chọn. |
163
- | `(outSort)` | `ISortEvent` | Emit khi người dùng thực hiện sắp xếp cột. |
164
- | `(outLoadMore)` | `ILoadMoreEvent` | Emit khi cần tải thêm dữ liệu (scroll infinity). |
165
- | `(outClickButtonAction)` | `ITabelButtonActionEvent` | Emit khi click vào button trong template cell. |
166
- | `(outLoading)` | `boolean` | Emit trạng thái loading của component. |
167
- | `(outTotalItem)` | `number` | Emit tổng số lượng bản ghi hiện có. |
168
- | `(outClickBarButton)` | `IButtonBarEvent` | Emit khi click vào nút trên thanh công cụ. |
483
+ ### @Input()
484
+
485
+ | Input | Type | Default | Mô tả | Ví dụ |
486
+ |---|---|---|---|---|
487
+ | `[httpRequestData]` | `IHttpRequestConfig` | `undefined` | **(Khuyên dùng)** Cấu hình service để component tự gọi API, phân trang, sort. | `[httpRequestData]="requestConfig"` |
488
+ | `[httpRequestDataFooter]` | `IHttpRequestConfig` | `undefined` | Cấu hình service gọi API riêng cho Footer. | `[httpRequestDataFooter]="footerConfig"` |
489
+ | `[headerLeft]` | `Array<ITableHeaderConfig>` | `[]` | Cột ghim cố định bên trái (Sticky). | `[headerLeft]="pinnedCols"` |
490
+ | `[headerRight]` | `Array<ITableHeaderConfig>` | `[]` | Cột chính hiển thị trong vùng cuộn. | `[headerRight]="mainCols"` |
491
+ | `[footerLeft]` | `Array<ITableFooterConfig>` | `undefined` | Cấu hình footer phần ghim trái. | `[footerLeft]="footerLeftCols"` |
492
+ | `[footerRight]` | `Array<ITableFooterConfig>` | `undefined` | Cấu hình footer phần cuộn. | `[footerRight]="footerRightCols"` |
493
+ | `[showFooter]` | `boolean` | `undefined` | Hiển thị footer của bảng. | `[showFooter]="true"` |
494
+ | `[customPositionFooter]` | `'top' \| 'bottom'` | `'bottom'` | Vị trí footer (trên hoặc dưới bảng). | `[customPositionFooter]="'top'"` |
495
+ | `[ignoreBorderFooter]` | `boolean` | `undefined` | Ẩn border của footer. | `[ignoreBorderFooter]="true"` |
496
+ | `[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) }"` |
497
+ | `[filter]` | `TYPE_DATA_FILTER_TABLE` | `{ filterData: {} }` | Filter/search. Thay đổi `filterData` để trigger gọi API lại. | `[filter]="{ filterData: { keySearch: 'abc' } }"` |
498
+ | `[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'"` |
499
+ | `[paginationSetting]` | `IPaginationConfig` | `undefined` | Tuỳ chỉnh giao diện phân trang (số trang, ô nhảy trang, ...). | `[paginationSetting]="{ showTotalPage: true }"` |
500
+ | `[fieldKey]` | `string` | `'id'` | Trường định danh duy nhất mỗi hàng. | `[fieldKey]="'userId'"` |
501
+ | `[barButtons]` | `Array<IButton>` | `undefined` | Nút thao tác xuất hiện trên bar khi có row được chọn. | `[barButtons]="actionButtons"` |
502
+ | `[configTemplateItemCollapseExpand]` | `IConfigTemplateItemCollapseExpand` | `undefined` | Cấu hình template Collapse/Expand row. | `[configTemplateItemCollapseExpand]="expandConfig"` |
503
+ | `[noDataConfig]` | `ITableNoDataConfig` | `{textNoData: 'i18n_no_data_yet', textSearchNoData: 'i18n_no_result'}` | Cấu hình text/icon khi không dữ liệu. | `[noDataConfig]="{ textNoData: 'Chưa có dữ liệu' }"` |
504
+ | `[useScrollMeasureColumn]` | `boolean` | `undefined` | Bật Virtual Scroll cột ngang (ảo hoá render cột). | `[useScrollMeasureColumn]="true"` |
505
+ | `[functionGetWidthItem]` | `() => Promise<number>` | `undefined` | **(Bắt buộc khi useScrollMeasureColumn=true)** Hàm trả về px mỗi cột. | `[functionGetWidthItem]="getColWidth"` |
506
+ | `[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 }"` |
507
+ | `[optimizeTableRenderByOnViewport]` | `boolean` | `undefined` | Tối ưu render cell khi nằm trong viewport. | `[optimizeTableRenderByOnViewport]="true"` |
508
+ | `[bufferAmount]` | `number` | `5` | Số item buffer cho Virtual Scroller (tối đa 25). | `[bufferAmount]="10"` |
509
+ | `[enableUnequalChildrenSizes]` | `boolean` | `false` | Hỗ trợ item có kích thước không đều (Virtual Scroller). | `[enableUnequalChildrenSizes]="true"` |
510
+ | `[totalItem]` | `number` | `undefined` | Tổng số item (override tính toán từ paging). | `[totalItem]="500"` |
511
+ | `[defaultKeysSelected]` | `Array<any>` | `undefined` | Danh sách key được chọn mặc định khi khởi tạo. | `[defaultKeysSelected]="['1','2']"` |
512
+ | `[maxItemSelected]` | `number` | `100` | Số lượng item tối đa được chọn. | `[maxItemSelected]="50"` |
513
+ | `[autoEmitKeysSelectedWithItems]` | `boolean` | `undefined` | Khi `true`, `outKeysSelected` emit cả items (không chỉ keys). | `[autoEmitKeysSelectedWithItems]="true"` |
514
+ | `[configSelectMoreItem]` | `IConfigSelectMoreItem` | `undefined` | Dropdown chọn số lượng item từ droplist (tương tự "Chọn X bản ghi"). | `[configSelectMoreItem]="moreItemConfig"` |
515
+ | `[disableCheckbox]` | `boolean` | `false` | hiệu hoá tất cả checkbox. | `[disableCheckbox]="isReadOnly()"` |
516
+ | `[sortLocal]` | `boolean` | `undefined` | Sort tại client, không gọi API. | `[sortLocal]="true"` |
517
+ | `[filterOrSortLocal]` | `TYPE_TABLE_FILTER` | `undefined` | Hàm filter/sort local tuỳ chỉnh. | `[filterOrSortLocal]="customFilterFn"` |
518
+ | `[ignoreBar]` | `boolean` | `undefined` | Ẩn toàn bộ bar (kể cả thanh đếm). | `[ignoreBar]="true"` |
519
+ | `[ignoreClassBgHeader]` | `boolean` | `undefined` | Bỏ class `bg-white` mặc định của header. | `[ignoreClassBgHeader]="true"` |
520
+ | `[isDashBorder]` | `boolean` | `undefined` | Đường viền dạng nét đứt. | `[isDashBorder]="true"` |
521
+ | `[ignoreBorderItem]` | `boolean` | `undefined` | Ẩn border giữa các row. | `[ignoreBorderItem]="true"` |
522
+ | `[ignoreBorderItemLast]` | `boolean` | `undefined` | Ẩn border của row cuối. | `[ignoreBorderItemLast]="true"` |
523
+ | `[isHiddenHeaderWhenNodata]` | `boolean` | `undefined` | Ẩn header khi không có dữ liệu. | `[isHiddenHeaderWhenNodata]="true"` |
524
+ | `[showScrollTablePinnedIfNoData]` | `boolean` | `undefined` | Vẫn hiển thị phần cuộn khi không có data. | `[showScrollTablePinnedIfNoData]="true"` |
525
+ | `[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"` |
526
+ | `[templateNoData]` | `TemplateRef` | `undefined` | Custom template khi không có dữ liệu. | `[templateNoData]="noDataTpl"` |
527
+ | `[templateNoResult]` | `TemplateRef` | `undefined` | Custom template khi không có kết quả tìm kiếm. | `[templateNoResult]="noResultTpl"` |
528
+ | `[timeHighlighNewItem]` | `number` | `2000` | Thời gian (ms) highlight item mới thêm vào. | `[timeHighlighNewItem]="3000"` |
529
+ | `[labelBarNoSelectItem]` | `string` | `'i18n_total_quantity'` | Label hiển thị khi không có item nào được chọn. | `[labelBarNoSelectItem]="'Tổng số'"` |
530
+ | `[labelBarButtons]` | `string` | `'i18n_number_item_selected'` | Label số item đã chọn trên bar. | `[labelBarButtons]="'i18n_selected'"` |
531
+ | `[classHeaderContainerInclude]` | `string` | `undefined` | Class thêm vào container header. | `classHeaderContainerInclude="!h-[80px]"` |
532
+ | `[classHeaderLeftInclude]` | `string` | `undefined` | Class thêm vào header left. | `classHeaderLeftInclude="bg-blue-50"` |
533
+ | `[classBodyInclude]` | `string` | `''` | Class thêm vào vùng body. | `classBodyInclude="max-h-[400px]"` |
534
+ | `[classFooterInclude]` | `string` | `''` | Class thêm vào footer. | `classFooterInclude="font-bold"` |
535
+ | `[classTableContainerInclude]` | `string` | `''` | Class thêm vào container bảng. | `classTableContainerInclude="rounded-lg"` |
536
+ | `[classBarInclude]` | `string` | `'justify-between mb-[16px]'` | Class thêm vào bar. | `classBarInclude="justify-end"` |
537
+ | `[classLabelBarButtons]` | `string` | `''` | Class cho label số item đã chọn. | `classLabelBarButtons="text-blue-500"` |
538
+ | `[classLabelBarNoSelectItem]` | `string` | `undefined` | Class cho label khi không chọn item. | `classLabelBarNoSelectItem="text-gray-500"` |
539
+
540
+ ### @Output()
541
+
542
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
543
+ |---|---|---|---|---|
544
+ | `(outSort)` | `ISortEvent` | Emit khi người dùng thay đổi sort cột. | `handlerSort(e: ISortEvent): void { e.stopPropagation(); }` | `(outSort)="handlerSort($event)"` |
545
+ | `(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)"` |
546
+ | `(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)"` |
547
+ | `(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)"` |
548
+ | `(outLoading)` | `boolean` | Emit trạng thái loading của component. | `handlerLoading(loading: boolean): void { }` | `(outLoading)="handlerLoading($event)"` |
549
+ | `(outTotalItem)` | `number` | Emit tổng số bản ghi từ API. | `handlerTotalItem(total: number): void { this.total.set(total); }` | `(outTotalItem)="handlerTotalItem($event)"` |
550
+ | `(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)"` |
551
+ | `(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)"` |
552
+ | `(outLoadDataError)` | `IHttpResponseError` | Emit khi gọi API thất bại. | `handlerLoadError(err: IHttpResponseError): void { }` | `(outLoadDataError)="handlerLoadError($event)"` |
553
+ | `(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)"` |
554
+
555
+ ---
556
+
557
+ ## FunctionsControl API
558
+
559
+ Nhận về qua `(outFunctionsControl)` để điều khiển bảng từ component cha.
560
+
561
+ ```typescript
562
+ import { ITableFunctionControlEvent } from '@libs-ui/components-table';
563
+
564
+ private tableCtrl?: ITableFunctionControlEvent;
565
+
566
+ handlerFunctionsControl(ctrl: ITableFunctionControlEvent): void {
567
+ this.tableCtrl = ctrl;
568
+ }
569
+
570
+ // Ví dụ sử dụng:
571
+ refreshTable(): void {
572
+ this.tableCtrl?.callApiByService(true); // Tải lại từ trang 1
573
+ }
574
+
575
+ resetTable(): void {
576
+ this.tableCtrl?.reset(true); // Reset + xóa sort
577
+ }
578
+
579
+ addItemToTop(newItem: any): void {
580
+ this.tableCtrl?.addItems(
581
+ convertObjectToSignal([newItem]),
582
+ true, // addFirst = true
583
+ );
584
+ }
585
+
586
+ removeItem(id: string): void {
587
+ this.tableCtrl?.removeItems([id]);
588
+ }
589
+
590
+ clearSelection(): void {
591
+ this.tableCtrl?.resetKeySelected();
592
+ }
593
+
594
+ getCount(): number {
595
+ return this.tableCtrl?.getCount() ?? 0;
596
+ }
597
+
598
+ jumpToPage(page: number): void {
599
+ this.tableCtrl?.changePagination(page);
600
+ }
601
+ ```
602
+
603
+ | Hàm | Signature | Mô tả |
604
+ |---|---|---|
605
+ | `reset` | `(clearSort?: boolean) => void` | Reset toàn bộ bảng về trạng thái ban đầu. |
606
+ | `callApiByService` | `(firstCall: boolean, resetAfterCallApi?: boolean) => void` | Gọi lại API. `firstCall=true` → tải từ trang 1. |
607
+ | `resetScroll` | `(left?: number, top?: number) => void` | Reset vị trí scroll. |
608
+ | `addItems` | `(items: TYPE_DATA_TABLE, addFirst: boolean, ...) => void` | Thêm items vào bảng. |
609
+ | `removeItems` | `(keys: Array<unknown>, ...) => void` | Xóa items theo key. |
610
+ | `resetKeySelected` | `() => void` | Xóa toàn bộ selection. |
611
+ | `getCount` | `() => number` | Lấy tổng số item hiện tại. |
612
+ | `getStore` | `() => Array<WritableSignal<TYPE_OBJECT>>` | Lấy toàn bộ store (kể cả item đã scroll qua). |
613
+ | `getItems` | `() => Array<WritableSignal<TYPE_OBJECT>>` | Lấy items đang hiển thị. |
614
+ | `hightLightItem` | `(items: Array<TYPE_ITEM_DATA_TABLE>) => Promise<void>` | Highlight item (hiệu ứng màu). |
615
+ | `changePagination` | `(page?: number) => Promise<void>` | Nhảy tới trang cụ thể. |
616
+ | `setCountPagingStore` | `(count: number) => void` | Cập nhật tổng số item trong paging store. |
617
+ | `detectChanges` | `() => void` | Trigger change detection thủ công. |
618
+
619
+ ---
169
620
 
170
621
  ## Types & Interfaces
171
622
 
172
623
  ### ITableHeaderConfig
173
624
 
174
625
  ```typescript
626
+ import { ITableHeaderConfig } from '@libs-ui/components-table';
627
+
175
628
  export interface ITableHeaderConfig {
176
- label?: string | TYPE_OBJECT;
177
- hasCheckbox?: boolean; // checkbox chọn row hay không
178
- hasCheckboxAll?: boolean; // checkbox "Chọn tất cả" hay không
179
- hasSort?: boolean; // nút sort hay không
180
- orderby?: string; // Field dùng để sort
181
- colTemplateConfig?: Array<ITableTemplateConfig>; // Cấu hình hiển thị cell
629
+ label?: string | TYPE_OBJECT; // Label header (hỗ trợ i18n object)
630
+ hasSort?: boolean; // Hiện nút sort
631
+ orderby?: string; // Field dùng để sort
632
+ hasCheckbox?: boolean; // Hiện checkbox
633
+ hasCheckboxAll?: boolean; // Hiện checkbox "Chọn tất cả"
634
+ disableCheckbox?: boolean; // Disable checkbox
635
+ colTemplateConfig?: Array<ITableTemplateConfig>; // Cấu hình hiển thị cell
636
+ rowsConfig?: { // Multi-row header
637
+ classContainerRows?: string;
638
+ rows: Array<{ classRow?: string; cols: Array<ITableHeaderConfigCol> }>;
639
+ };
182
640
  ngStyle?: TYPE_OBJECT;
183
641
  ngClass?: TYPE_OBJECT;
642
+ checkConditionCheckBoxHidden?: TYPE_FUNCTION<boolean>;
643
+ isShowIndexOnRow?: boolean; // Hiện số thứ tự dòng
644
+ defaultMode?: TYPE_SORT_TYPE; // Mode sort mặc định ('asc' | 'desc')
645
+ labelDescription?: { content: string; classInclude?: string };
646
+ tooltipsLeft?: Array<IPopover>;
647
+ tooltipsRight?: Array<IPopover>;
184
648
  }
185
649
  ```
186
650
 
187
- ### ITableTemplateConfig
651
+ ### ITableTemplateConfig & ITableFieldTemplateConfig
188
652
 
189
653
  ```typescript
654
+ import { ITableTemplateConfig, ITableFieldTemplateConfig } from '@libs-ui/components-table';
655
+
190
656
  export interface ITableTemplateConfig {
191
- cssWrapper: string; // Class CSS cho wrapper của cell
192
- fieldsConfig: Array<ITableFieldTemplateConfig>; // Danh sách các field hiển thị trong 1 cell
657
+ cssWrapper: string; // Class CSS cho wrapper của cell
658
+ fieldsConfig: Array<ITableFieldTemplateConfig>;
659
+ }
660
+
661
+ export interface ITableFieldTemplateConfig {
662
+ field: string; // Tên field trong data object
663
+ instance?: 'tooltip' | 'line-clamp' | 'buttons' | 'button-status'
664
+ | 'switch' | 'badge' | 'other-action-show-popup'
665
+ | 'shape-style' | 'image' | 'avatar' | 'button-action-show-popup';
666
+ parseValue?: TYPE_FUNCTION; // Custom render → trả về Observable<string> (HTML)
667
+ action?: (dataField, item, itemSelected?) => any;
668
+ getAvatarConfig?: TYPE_FUNCTION<IAvatarConfig | undefined>;
669
+ buttons?: Array<WritableSignal<IButton>>; // instance = 'buttons'
670
+ getButtonsByItem?: TYPE_FUNCTION<Array<WritableSignal<IButton>>>;
671
+ showButtonHoverMode?: boolean; // Chỉ hiện button khi hover
672
+ switchAction?: (switchData: ISwitchEvent, item) => void; // instance = 'switch'
673
+ getDisableValueSwitch?: TYPE_FUNCTION;
674
+ getActiveValueSwitch?: TYPE_FUNCTION;
675
+ lineClampConfig?: ILineClampConfig; // instance = 'line-clamp'
676
+ ngStyle?: TYPE_OBJECT;
677
+ getNgStyle?: TYPE_FUNCTION;
678
+ ngClass?: TYPE_OBJECT;
679
+ getNgClass?: TYPE_FUNCTION;
680
+ rows?: Array<ITableTemplateConfig>; // Sub-rows trong cell
681
+ getComponentOutlet?: () => Observable<any>;
682
+ getDataComponentOutlet?: TYPE_FUNCTION;
683
+ }
684
+ ```
685
+
686
+ ### IConfigTemplateItemCollapseExpand
687
+
688
+ ```typescript
689
+ import { IConfigTemplateItemCollapseExpand } from '@libs-ui/components-table';
690
+
691
+ export interface IConfigTemplateItemCollapseExpand {
692
+ fieldGetDataExpand?: string; // Trường chứa dữ liệu con trong mỗi row
693
+ cssWrapper?: string;
694
+ templateCssWrapper?: string;
695
+ templateCssWrapperHost?: string;
696
+ colTemplateConfig?: Array<ITableTemplateConfig>;
193
697
  }
698
+ // Mỗi item trong data cần có: specificExpand: boolean (quản lý state đóng/mở)
194
699
  ```
195
700
 
196
701
  ### TYPE_NEW_DATA_TABLE
197
702
 
198
703
  ```typescript
199
- export type TYPE_NEW_DATA_TABLE<T = TYPE_OBJECT> = {
200
- data: WritableSignal<Array<WritableSignal<T>>>;
201
- addToLastList?: boolean;
704
+ import { TYPE_NEW_DATA_TABLE } from '@libs-ui/components-table';
705
+
706
+ // Cách tạo dữ liệu tĩnh:
707
+ import { convertObjectToSignal } from '@libs-ui/utils';
708
+
709
+ const data: TYPE_NEW_DATA_TABLE = {
710
+ data: convertObjectToSignal([
711
+ { id: '1', name: 'Item 1' },
712
+ { id: '2', name: 'Item 2' },
713
+ ]),
714
+ addToLastList: false, // true = thêm xuống cuối, false = reset và set mới
715
+ };
716
+ ```
717
+
718
+ ### IHttpRequestConfig (httpRequestData)
719
+
720
+ ```typescript
721
+ import { IHttpRequestConfig } from '@libs-ui/services-http-request';
722
+
723
+ // Service phải có method trả về Observable với cấu trúc:
724
+ // { data: Array<any>, paging?: { page, pageSize, total_items, total_pages } }
725
+
726
+ const requestConfig: IHttpRequestConfig = {
727
+ objectInstance: myService, // Instance của service (inject qua inject())
728
+ functionName: 'getList', // Tên method trong service
729
+ argumentsValue: [new HttpParams()], // Mảng tham số truyền vào method
202
730
  };
203
731
  ```
204
732
 
205
- ## Công nghệ
733
+ ---
734
+
735
+ ## Lưu ý quan trọng
736
+
737
+ ⚠️ **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.
738
+
739
+ ⚠️ **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.
740
+
741
+ ⚠️ **Collapse/Expand**: Item data phải có trường `specificExpand: boolean`. Toggle bằng cách `item.update(val => ({ ...val, specificExpand: !val.specificExpand }))`.
742
+
743
+ ⚠️ **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.
206
744
 
207
- | Technology | Version | Purpose |
208
- | ---------------- | ------- | ------------------------- |
209
- | Angular | 18+ | Framework |
210
- | Virtual Scroller | 15+ | Hiển thị dữ liệu lớn |
211
- | Angular Signals | - | Quản lý trạng thái nội bộ |
212
- | TailwindCSS | 3.x | Styling chính |
745
+ ---
213
746
 
214
747
  ## Demo
215
748