@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
|
|
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à
|
|
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 và cơ 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)**:
|
|
12
|
-
- ✅ **Virtual Scrolling**: Tối ưu hiệu năng cho danh sách hàng
|
|
13
|
-
- ✅ **Phân trang
|
|
14
|
-
- ✅ **
|
|
15
|
-
- ✅ **Collapse/Expand Row**:
|
|
16
|
-
- ✅ **
|
|
17
|
-
- ✅ **
|
|
18
|
-
- ✅ **
|
|
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 có 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.
|
|
19
21
|
|
|
20
22
|
## Khi nào sử dụng
|
|
21
23
|
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
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
|
|
51
|
+
### 1. Standard Table (Virtual Scrolling + httpRequestData)
|
|
48
52
|
|
|
49
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
205
|
+
---
|
|
72
206
|
|
|
73
|
-
|
|
207
|
+
### 4. Advanced Layout (Custom Header Height + Footer Position)
|
|
74
208
|
|
|
75
209
|
```html
|
|
76
210
|
<libs_ui-components-table
|
|
77
|
-
[headerLeft]="
|
|
78
|
-
[
|
|
79
|
-
[
|
|
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
|
-
|
|
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
|
-
|
|
233
|
+
### 5. Collapse / Expand Row (KPI Report)
|
|
85
234
|
|
|
86
235
|
```html
|
|
87
236
|
<libs_ui-components-table
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
[
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
###
|
|
136
|
-
|
|
137
|
-
|
|
|
138
|
-
|
|
139
|
-
| `[httpRequestData]`
|
|
140
|
-
| `[
|
|
141
|
-
| `[
|
|
142
|
-
| `[
|
|
143
|
-
| `[
|
|
144
|
-
| `[
|
|
145
|
-
| `[
|
|
146
|
-
| `[
|
|
147
|
-
| `[
|
|
148
|
-
| `[
|
|
149
|
-
| `[filter]`
|
|
150
|
-
| `[activityLoadData]`
|
|
151
|
-
| `[
|
|
152
|
-
| `[
|
|
153
|
-
| `[
|
|
154
|
-
| `[
|
|
155
|
-
| `[
|
|
156
|
-
| `[
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
| `
|
|
163
|
-
| `
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
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 có 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` | Vô 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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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;
|
|
192
|
-
fieldsConfig: Array<ITableFieldTemplateConfig>;
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|