@libs-ui/components-table 0.2.356-9 → 0.2.357-1

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