@libs-ui/components-dropdown 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,52 +1,39 @@
1
1
  # @libs-ui/components-dropdown
2
2
 
3
- > Component dropdown đa năng hỗ trợ nhiều chế độ hiển thị: text, radio, checkbox, group. Tích hợp sẵn tìm kiếm, validation, popover, tabs và điều khiển từ bên ngoài.
4
-
5
- **Version:** `0.2.355-14`
3
+ > Component dropdown đa năng hỗ trợ nhiều chế độ hiển thị: text, radio, checkbox, group, tree, JSON tree. Tích hợp sẵn tìm kiếm, validation, popover overlay, tabs phân loại và điều khiển từ bên ngoài qua FunctionControl.
6
4
 
7
5
  ## Giới thiệu
8
6
 
9
- `LibsUiComponentsDropdownComponent` là một standalone Angular component cung cấp dropdown selection với nhiều chế độ hiển thị và tính năng nâng cao.
7
+ `LibsUiComponentsDropdownComponent` là một standalone Angular component cung cấp dropdown selection với nhiều chế độ hiển thị và tính năng nâng cao. Component tự động load dữ liệu từ API thông qua `IHttpRequestConfig`, hỗ trợ cả chọn đơn và chọn nhiều, đồng thời cung cấp `FunctionControl` để component cha có thể điều khiển (reset, refresh, validate) từ bên ngoài.
10
8
 
11
- ### Tính năng
9
+ ## Tính năng
12
10
 
13
- - ✅ Nhiều chế độ hiển thị: text, radio, checkbox, group
14
- - ✅ Tìm kiếm online/offline tích hợp sẵn
15
- - ✅ Validation (required, max items selected)
16
- - ✅ Điều khiển từ bên ngoài qua `IDropdownFunctionControlEvent`
17
- - ✅ Hỗ trợ tabs để phân loại dữ liệu
18
- - ✅ Tự động load dữ liệu từ API (httpRequestData)
19
- - ✅ Auto-select first item / all items
20
- - ✅ Hiển thị avatar, icon, image cho item
21
- - ✅ Custom content qua ng-content
22
- - ✅ OnPush Change Detection
23
- - ✅ Angular Signals
11
+ - ✅ Nhiều chế độ hiển thị: `text`, `radio`, `checkbox`, `group` (tree, JSON tree, personalize)
12
+ - ✅ Tìm kiếm online (gọi API) và offline (client-side) tích hợp sẵn
13
+ - ✅ Validation: required, giới hạn số lượng item được chọn tối đa
14
+ - ✅ Điều khiển từ bên ngoài qua `IDropdownFunctionControlEvent` (reset, refresh, setError, setItemSelectedByKey...)
15
+ - ✅ Hỗ trợ tabs để phân loại dữ liệu theo nhóm
16
+ - ✅ Tự động load auto-select item đầu tiên hoặc toàn bộ (autoSelectFirstItem / autoSelectAllItem)
17
+ - ✅ Hiển thị avatar, icon, image cho từng item trong danh sách
18
+ - ✅ Custom content qua `ng-content` (`[isNgContent]="true"`)
19
+ - ✅ Tùy chỉnh popover overlay (hướng, width, z-index, animation...)
20
+ - ✅ Lazy-load chi tiết item theo key (`httpRequestDetailItemById`)
21
+ - ✅ OnPush Change Detection + Angular Signals
24
22
 
25
23
  ## Khi nào sử dụng
26
24
 
27
- - Chọn một hoặc nhiều giá trị từ danh sách dữ liệu
25
+ - Chọn một hoặc nhiều giá trị từ danh sách dữ liệu API
28
26
  - Dropdown với tìm kiếm online/offline
29
- - Chọn dữ liệu dạng nhóm (group), cây (tree), JSON tree
27
+ - Chọn dữ liệu dạng nhóm (group), cây (tree), JSON tree nested nhiều cấp
30
28
  - Cần validation (required, max items)
31
- - Cần điều khiển dropdown từ component cha (reset, refresh, check valid)
32
- - Hiển thị dropdown với tabs để phân loại dữ liệu
33
- - Dropdown với custom content (ng-content)
34
-
35
- ## Lưu ý quan trọng
36
-
37
- - ⚠️ **Bắt buộc `[listConfig]`**: Phải cung cấp `[listConfig]` để dropdown hiển thị danh sách.
38
- - ⚠️ **Chế độ chọn**: Sử dụng `[listKeySelected]` cho chọn đơn (text/radio), `[listMultiKeySelected]` cho chọn nhiều (checkbox/group).
39
- - ⚠️ **Auto-load API**: Khi dùng `httpRequestData` trong `listConfig`, dropdown tự động gọi API để load dữ liệu.
40
- - ⚠️ **FunctionControl**: Output `outFunctionsControl` emit `IDropdownFunctionControlEvent` để điều khiển dropdown từ bên ngoài.
29
+ - Cần điều khiển dropdown từ component cha (reset, refresh, check valid, set error)
30
+ - Hiển thị dropdown với tabs để phân loại dữ liệu theo nguồn khác nhau
31
+ - Dropdown với custom trigger (ng-content) thay vì giao diện mặc định
41
32
 
42
33
  ## Cài đặt
43
34
 
44
35
  ```bash
45
- # npm
46
36
  npm install @libs-ui/components-dropdown
47
-
48
- # yarn
49
- yarn add @libs-ui/components-dropdown
50
37
  ```
51
38
 
52
39
  ## Import
@@ -66,266 +53,676 @@ import {
66
53
  @Component({
67
54
  standalone: true,
68
55
  imports: [LibsUiComponentsDropdownComponent],
69
- // ...
70
56
  })
71
- export class YourComponent {}
57
+ export class MyComponent {}
58
+ ```
59
+
60
+ ## Ví dụ sử dụng
61
+
62
+ ### 1. Basic — Dropdown chọn đơn (type: text)
63
+
64
+ ```typescript
65
+ import { Component, signal } from '@angular/core';
66
+ import { LibsUiComponentsDropdownComponent, IEmitSelectKey } from '@libs-ui/components-dropdown';
67
+ import { IListConfigItem } from '@libs-ui/components-list';
68
+ import { IHttpRequestConfig } from '@libs-ui/services-http-request';
69
+ import { escapeHtml, get, set, UtilsHttpParamsRequest } from '@libs-ui/utils';
70
+
71
+ @Component({
72
+ selector: 'app-example-basic',
73
+ standalone: true,
74
+ imports: [LibsUiComponentsDropdownComponent],
75
+ template: `
76
+ <libs_ui-components-dropdown
77
+ [labelConfig]="{ labelLeft: 'Chọn nhân viên', required: true }"
78
+ [listConfig]="listConfig"
79
+ [listSearchConfig]="{ noBorder: true }"
80
+ [listMaxItemShow]="5"
81
+ [convertItemSelected]="convertItemSelected"
82
+ [validRequired]="{}"
83
+ (outSelectKey)="handlerSelectKey($event)"
84
+ (outFunctionsControl)="handlerFunctionsControl($event)"
85
+ />
86
+ `,
87
+ })
88
+ export class ExampleBasicComponent {
89
+ private dropdownControl: IDropdownFunctionControlEvent | undefined;
90
+
91
+ readonly listConfig: IListConfigItem = {
92
+ type: 'text',
93
+ httpRequestData: signal<IHttpRequestConfig>({
94
+ objectInstance: myApiService,
95
+ functionName: 'getList',
96
+ argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
97
+ }),
98
+ configTemplateText: signal({
99
+ fieldKey: 'id',
100
+ notUseVirtualScroll: true,
101
+ getValue: (item: { name: string }) => escapeHtml(item.name),
102
+ }),
103
+ };
104
+
105
+ readonly convertItemSelected = (item: unknown): void => {
106
+ if (!item) return;
107
+ set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
108
+ };
109
+
110
+ handlerSelectKey(event: IEmitSelectKey | undefined): void {
111
+ event?.key; // id item đã chọn
112
+ event?.item; // object item đầy đủ
113
+ }
114
+
115
+ handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
116
+ this.dropdownControl = event;
117
+ }
118
+
119
+ async resetSelection(): Promise<void> {
120
+ await this.dropdownControl?.reset();
121
+ }
122
+ }
123
+ ```
124
+
125
+ ### 2. Dropdown chọn nhiều (type: checkbox)
126
+
127
+ ```typescript
128
+ import { Component, signal } from '@angular/core';
129
+ import { LibsUiComponentsDropdownComponent, IEmitMultiKey } from '@libs-ui/components-dropdown';
130
+ import { IListConfigItem } from '@libs-ui/components-list';
131
+ import { IHttpRequestConfig } from '@libs-ui/services-http-request';
132
+ import { escapeHtml, get, set, UtilsHttpParamsRequest } from '@libs-ui/utils';
133
+
134
+ @Component({
135
+ selector: 'app-example-checkbox',
136
+ standalone: true,
137
+ imports: [LibsUiComponentsDropdownComponent],
138
+ template: `
139
+ <libs_ui-components-dropdown
140
+ [labelConfig]="{ labelLeft: 'Chọn nhiều nhãn', required: true }"
141
+ [listConfig]="checkboxConfig"
142
+ [listSearchConfig]="{ noBorder: true }"
143
+ [listMaxItemShow]="5"
144
+ [(listMultiKeySelected)]="selectedKeys"
145
+ [convertItemSelected]="convertItemSelected"
146
+ [validRequired]="{}"
147
+ [validMaxItemSelected]="{ value: 3, message: 'Chỉ được chọn tối đa 3 mục' }"
148
+ (outSelectMultiKey)="handlerSelectMultiKey($event)"
149
+ />
150
+ `,
151
+ })
152
+ export class ExampleCheckboxComponent {
153
+ selectedKeys = signal<string[]>([]);
154
+
155
+ readonly checkboxConfig: IListConfigItem = {
156
+ type: 'checkbox',
157
+ httpRequestData: signal<IHttpRequestConfig>({
158
+ objectInstance: myApiService,
159
+ functionName: 'getList',
160
+ argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
161
+ }),
162
+ autoSelectFirstItem: false,
163
+ configTemplateCheckbox: signal({
164
+ fieldKey: 'id',
165
+ configButtonSelectAndUndSelectItem: signal({}),
166
+ getValue: (item: { name: string }) => escapeHtml(item.name),
167
+ }),
168
+ };
169
+
170
+ readonly convertItemSelected = (item: unknown): void => {
171
+ if (!item) return;
172
+ set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
173
+ };
174
+
175
+ handlerSelectMultiKey(event: IEmitMultiKey | undefined): void {
176
+ event?.keys; // mảng id đã chọn
177
+ event?.mapKeys; // mảng { key, item } đầy đủ
178
+ }
179
+ }
72
180
  ```
73
181
 
74
- ## dụ
182
+ ### 3. Dropdown dạng nhóm — Group Checkbox
183
+
184
+ ```typescript
185
+ import { Component, signal } from '@angular/core';
186
+ import { LibsUiComponentsDropdownComponent, IEmitMultiKey } from '@libs-ui/components-dropdown';
187
+ import { IListConfigItem } from '@libs-ui/components-list';
188
+ import { IHttpRequestConfig, returnListObject } from '@libs-ui/services-http-request';
189
+ import { escapeHtml, get, set } from '@libs-ui/utils';
190
+
191
+ @Component({
192
+ selector: 'app-example-group',
193
+ standalone: true,
194
+ imports: [LibsUiComponentsDropdownComponent],
195
+ template: `
196
+ <libs_ui-components-dropdown
197
+ [labelConfig]="{ labelLeft: 'Chọn theo nhóm', required: true }"
198
+ [listConfig]="groupConfig"
199
+ [listMaxItemShow]="5"
200
+ [convertItemSelected]="convertItemSelected"
201
+ (outSelectMultiKey)="handlerSelectMultiKey($event)"
202
+ />
203
+ `,
204
+ })
205
+ export class ExampleGroupComponent {
206
+ private readonly groupDataService = returnListObject([
207
+ {
208
+ id: 'nhom_a',
209
+ name: 'Nhóm A',
210
+ items: [
211
+ { id: 'a1', name: 'Mục A1' },
212
+ { id: 'a2', name: 'Mục A2' },
213
+ ],
214
+ },
215
+ {
216
+ id: 'nhom_b',
217
+ name: 'Nhóm B',
218
+ items: [{ id: 'b1', name: 'Mục B1' }],
219
+ },
220
+ ]);
221
+
222
+ readonly groupConfig: IListConfigItem = {
223
+ type: 'group',
224
+ httpRequestData: signal<IHttpRequestConfig>({
225
+ objectInstance: this.groupDataService,
226
+ functionName: 'list',
227
+ argumentsValue: [],
228
+ }),
229
+ configTemplateGroup: signal({
230
+ fieldKey: 'id',
231
+ fieldGetItems: 'items',
232
+ getLabelGroup: (group: { name: string }) => escapeHtml(group.name),
233
+ getMaxLevelGroup: () => 2,
234
+ getLabelItem: (item: { name: string }) => escapeHtml(item.name),
235
+ iconExpand: 'right',
236
+ isViewRadio: false,
237
+ }),
238
+ };
239
+
240
+ readonly convertItemSelected = (item: unknown): void => {
241
+ if (!item) return;
242
+ set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
243
+ };
244
+
245
+ handlerSelectMultiKey(event: IEmitMultiKey | undefined): void {
246
+ event?.keys;
247
+ }
248
+ }
249
+ ```
75
250
 
76
- ### Basic - Text
251
+ ### 4. Dropdown Group Radio — chọn đơn trong nhóm
77
252
 
78
253
  ```html
79
254
  <libs_ui-components-dropdown
80
- [labelConfig]="{ labelLeft: 'Chọn item', required: true }"
81
- [listConfig]="listConfig"
82
- [listSearchConfig]="{ noBorder: true }"
255
+ [labelConfig]="{ labelLeft: 'Chọn một mục trong nhóm', required: true }"
256
+ [listConfig]="groupRadioConfig"
83
257
  [listMaxItemShow]="5"
84
258
  [convertItemSelected]="convertItemSelected"
85
- (outSelectKey)="onSelectKey($event)"
259
+ (outSelectKey)="handlerSelectKey($event)"
86
260
  />
87
261
  ```
88
262
 
89
263
  ```typescript
90
- listConfig: IListConfigItem = {
91
- type: 'text',
92
- httpRequestData: signal<IHttpRequestConfig>({
93
- objectInstance: getConfigListDataDemo(),
94
- functionName: 'list',
95
- argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
96
- }),
97
- configTemplateText: signal({
264
+ readonly groupRadioConfig: IListConfigItem = {
265
+ type: 'group',
266
+ httpRequestData: signal<IHttpRequestConfig>(groupHttpConfig),
267
+ configTemplateGroup: signal({
98
268
  fieldKey: 'id',
99
- notUseVirtualScroll: true,
100
- getValue: (item) => escapeHtml(item.name),
269
+ fieldGetItems: 'items',
270
+ getLabelGroup: (group: { name: string }) => escapeHtml(group.name),
271
+ getMaxLevelGroup: () => 2,
272
+ getLabelItem: (item: { name: string }) => escapeHtml(item.name),
273
+ iconExpand: 'right',
274
+ isViewRadio: true, // chỉ khác group checkbox ở dòng này
101
275
  }),
102
276
  };
103
277
  ```
104
278
 
105
- ### Checkbox
279
+ ### 5. Dropdown Radio
106
280
 
107
281
  ```html
108
282
  <libs_ui-components-dropdown
109
- [labelConfig]="{ labelLeft: 'Chọn nhiều (checkbox)', required: true }"
110
- [listConfig]="checkboxConfig"
283
+ [labelConfig]="{ labelLeft: 'Chọn loại khách hàng', required: true }"
284
+ [listConfig]="radioConfig"
111
285
  [listMaxItemShow]="5"
112
286
  [convertItemSelected]="convertItemSelected"
113
- (outSelectMultiKey)="onSelectMultiKey($event)"
287
+ (outSelectKey)="handlerSelectKey($event)"
114
288
  />
115
289
  ```
116
290
 
117
- ### Radio
118
-
119
- ```html
120
- <libs_ui-components-dropdown
121
- [labelConfig]="{ labelLeft: 'Chọn item (radio)', required: true }"
122
- [listConfig]="radioConfig"
123
- [listMaxItemShow]="5"
124
- [convertItemSelected]="convertItemSelected"
125
- (outSelectKey)="onSelectKey($event)"
126
- />
291
+ ```typescript
292
+ readonly radioConfig: IListConfigItem = {
293
+ type: 'radio',
294
+ httpRequestData: signal<IHttpRequestConfig>({
295
+ objectInstance: myApiService,
296
+ functionName: 'getList',
297
+ argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
298
+ }),
299
+ configTemplateRadio: signal({
300
+ fieldKey: 'id',
301
+ getValue: (item: { name: string }) => escapeHtml(item.name),
302
+ }),
303
+ };
127
304
  ```
128
305
 
129
- ### Interactive - Controls
306
+ ### 6. Dropdown với FunctionControl — điều khiển từ bên ngoài
130
307
 
131
308
  ```html
309
+ <div class="flex gap-[8px] mb-[16px]">
310
+ <button (click)="handlerReset()">Reset</button>
311
+ <button (click)="handlerCheckValid()">Check Valid</button>
312
+ <button (click)="handlerRefresh()">Refresh</button>
313
+ <button (click)="handlerSetError()">Set Error</button>
314
+ </div>
315
+
132
316
  <libs_ui-components-dropdown
133
- [labelConfig]="{ labelLeft: 'Interactive', required: true }"
317
+ [labelConfig]="{ labelLeft: 'Dropdown có điều khiển', required: true }"
134
318
  [listConfig]="listConfig"
135
319
  [listMaxItemShow]="5"
136
320
  [validRequired]="{}"
137
- (outFunctionsControl)="onFunctionControl($event)"
321
+ (outFunctionsControl)="handlerFunctionsControl($event)"
322
+ (outValidEvent)="handlerValidEvent($event)"
138
323
  />
139
324
  ```
140
325
 
141
326
  ```typescript
142
- dropdownControl: IDropdownFunctionControlEvent | undefined;
327
+ import { IDropdownFunctionControlEvent, IEmitSelectKey } from '@libs-ui/components-dropdown';
328
+
329
+ private dropdownControl: IDropdownFunctionControlEvent | undefined;
143
330
 
144
- onFunctionControl(event: IDropdownFunctionControlEvent) {
331
+ handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
145
332
  this.dropdownControl = event;
146
333
  }
147
334
 
148
- async resetDropdown() {
335
+ handlerValidEvent(isValid: boolean): void {
336
+ // true khi hợp lệ, false khi có lỗi validation
337
+ }
338
+
339
+ async handlerReset(): Promise<void> {
149
340
  await this.dropdownControl?.reset();
150
341
  }
151
342
 
152
- async checkValid() {
343
+ async handlerCheckValid(): Promise<void> {
153
344
  const isValid = await this.dropdownControl?.checkIsValid();
154
- console.log('Valid:', isValid);
345
+ // isValid: true/false
155
346
  }
156
347
 
157
- async refreshList() {
348
+ async handlerRefresh(): Promise<void> {
158
349
  await this.dropdownControl?.refreshList();
159
350
  }
351
+
352
+ async handlerSetError(): Promise<void> {
353
+ await this.dropdownControl?.setError?.('i18n_error_message');
354
+ }
355
+
356
+ async handlerSetItemByKey(): Promise<void> {
357
+ await this.dropdownControl?.setItemSelectedByKey('abc-123');
358
+ }
359
+
360
+ async handlerUpdateLabel(): Promise<void> {
361
+ await this.dropdownControl?.updateLabelItemSelected('Tên hiển thị mới');
362
+ }
363
+ ```
364
+
365
+ ### 7. Dropdown với Tabs phân loại dữ liệu
366
+
367
+ ```typescript
368
+ import { IDropdownTabsItem } from '@libs-ui/components-dropdown';
369
+
370
+ readonly tabsConfig: IDropdownTabsItem[] = [
371
+ {
372
+ key: 'all',
373
+ name: 'Tất cả',
374
+ httpRequestData: {
375
+ objectInstance: allApiService,
376
+ functionName: 'getList',
377
+ argumentsValue: [],
378
+ },
379
+ },
380
+ {
381
+ key: 'active',
382
+ name: 'Hoạt động',
383
+ httpRequestData: {
384
+ objectInstance: activeApiService,
385
+ functionName: 'getList',
386
+ argumentsValue: [],
387
+ },
388
+ },
389
+ ];
390
+ ```
391
+
392
+ ```html
393
+ <libs_ui-components-dropdown
394
+ [labelConfig]="{ labelLeft: 'Chọn với tab phân loại', required: true }"
395
+ [listConfig]="listConfig"
396
+ [tabsConfig]="tabsConfig"
397
+ [(tabKeyActive)]="activeTabKey"
398
+ [listMaxItemShow]="5"
399
+ [convertItemSelected]="convertItemSelected"
400
+ (outSelectKey)="handlerSelectKey($event)"
401
+ (outChangeTabKeyActive)="handlerChangeTab($event)"
402
+ />
403
+ ```
404
+
405
+ ```typescript
406
+ protected activeTabKey = signal<string>('all');
407
+
408
+ handlerChangeTab(key: string | undefined): void {
409
+ // key: tab vừa được chọn
410
+ }
160
411
  ```
161
412
 
162
- ### States
413
+ ### 8. Dropdown trạng thái: Disable / Readonly / Validation
163
414
 
164
415
  ```html
165
416
  <!-- Disable -->
166
417
  <libs_ui-components-dropdown
167
- [labelConfig]="{ labelLeft: 'Disable' }"
168
- [disable]="true"
418
+ [labelConfig]="{ labelLeft: 'Trạng thái disable' }"
169
419
  [listConfig]="listConfig"
420
+ [disable]="true"
170
421
  />
171
422
 
172
423
  <!-- Readonly -->
173
424
  <libs_ui-components-dropdown
174
- [labelConfig]="{ labelLeft: 'Readonly' }"
425
+ [labelConfig]="{ labelLeft: 'Trạng thái readonly' }"
426
+ [listConfig]="listConfig"
175
427
  [readonly]="true"
428
+ [(listKeySelected)]="selectedKey"
429
+ />
430
+
431
+ <!-- Validation required -->
432
+ <libs_ui-components-dropdown
433
+ [labelConfig]="{ labelLeft: 'Bắt buộc chọn', required: true }"
434
+ [listConfig]="listConfig"
435
+ [validRequired]="{ message: 'i18n_field_required' }"
436
+ [showError]="true"
437
+ />
438
+
439
+ <!-- Validation max items -->
440
+ <libs_ui-components-dropdown
441
+ [labelConfig]="{ labelLeft: 'Chọn tối đa 3', required: true }"
442
+ [listConfig]="checkboxConfig"
443
+ [validMaxItemSelected]="{ value: 3, message: 'Chỉ được chọn tối đa {value} mục', interpolateParams: { value: 3 } }"
444
+ />
445
+ ```
446
+
447
+ ### 9. Dropdown với tìm kiếm online (Search Online)
448
+
449
+ ```html
450
+ <libs_ui-components-dropdown
451
+ [labelConfig]="{ labelLeft: 'Tìm kiếm online', required: true }"
452
+ [listConfig]="listConfig"
453
+ [isSearchOnline]="true"
454
+ [listSearchConfig]="{ noBorder: true, placeholder: 'Gõ keyword để tìm kiếm...' }"
455
+ [listMaxItemShow]="8"
456
+ [convertItemSelected]="convertItemSelected"
457
+ (outSelectKey)="handlerSelectKey($event)"
458
+ />
459
+ ```
460
+
461
+ ### 10. Dropdown lazy-load chi tiết item theo key
462
+
463
+ Dùng khi dropdown chỉ nhận được key (id) từ server, cần gọi API riêng để lấy thông tin hiển thị.
464
+
465
+ > 🔴 **Lỗi phổ biến:** Truyền `[(listKeySelected)]="selectedKey"` (key có sẵn) **NHƯNG quên** `[httpRequestDetailItemById]`. Dropdown chỉ có key, không có item → label hiển thị trắng. BẮT BUỘC cấu hình `[httpRequestDetailItemById]` như ví dụ dưới.
466
+
467
+ ```html
468
+ <!-- ❌ SAI — có key nhưng thiếu httpRequestDetailItemById → label trắng -->
469
+ <libs_ui-components-dropdown
470
+ [labelConfig]="{ labelLeft: 'Thiếu detail config' }"
471
+ [listConfig]="listConfig"
472
+ [(listKeySelected)]="selectedKey"
473
+ [convertItemSelected]="convertItemSelected"
474
+ />
475
+ ```
476
+
477
+ ```html
478
+ <!-- ✅ ĐÚNG -->
479
+ <libs_ui-components-dropdown
480
+ [labelConfig]="{ labelLeft: 'Dropdown lazy-load detail' }"
481
+ [listConfig]="listConfig"
482
+ [(listKeySelected)]="selectedKey"
483
+ [httpRequestDetailItemById]="httpRequestDetailConfig"
484
+ [convertItemSelected]="convertItemSelected"
485
+ (outSelectKey)="handlerSelectKey($event)"
486
+ />
487
+ ```
488
+
489
+ ```typescript
490
+ readonly httpRequestDetailConfig: IHttpRequestConfig = {
491
+ objectInstance: detailApiService,
492
+ functionName: 'getDetailByKey',
493
+ argumentsValue: [],
494
+ guideAutoUpdateArgumentsValue: {
495
+ paging: {},
496
+ detailById: {
497
+ fieldGetValue: '',
498
+ fieldUpdate: '[0]',
499
+ },
500
+ },
501
+ };
502
+
503
+ selectedKey = signal<string>('item-id-123'); // sẽ tự gọi API để load tên hiển thị
504
+ ```
505
+
506
+ ### 11. Dropdown không load list trước khi search
507
+
508
+ ```html
509
+ <libs_ui-components-dropdown
510
+ [labelConfig]="{ labelLeft: 'Gõ keyword để bắt đầu load' }"
511
+ [listConfig]="listConfig"
512
+ [isSearchOnline]="true"
513
+ [listHiddenInputSearch]="false"
514
+ [listSearchConfig]="{ noBorder: true, placeholder: 'Nhập để tìm kiếm...' }"
515
+ [dropdownIgnoreNotSearch]="true"
516
+ />
517
+ ```
518
+
519
+ ### 12. Dropdown với Custom Popover Config
520
+
521
+ ```typescript
522
+ import { IPopoverCustomConfig } from '@libs-ui/components-dropdown';
523
+
524
+ readonly popoverConfig: IPopoverCustomConfig = {
525
+ widthByParent: false,
526
+ maxWidth: 500,
527
+ maxHeight: 400,
528
+ direction: 'bottom',
529
+ ignoreArrow: true,
530
+ position: { mode: 'start', distance: 0 },
531
+ animationConfig: { time: 200, distance: 8 },
532
+ };
533
+ ```
534
+
535
+ ```html
536
+ <libs_ui-components-dropdown
537
+ [labelConfig]="{ labelLeft: 'Dropdown custom popover' }"
176
538
  [listConfig]="listConfig"
539
+ [popoverCustomConfig]="popoverConfig"
540
+ [zIndex]="1050"
541
+ [convertItemSelected]="convertItemSelected"
542
+ (outSelectKey)="handlerSelectKey($event)"
177
543
  />
178
544
  ```
179
545
 
180
- ## API
181
-
182
- ### libs_ui-components-dropdown
183
-
184
- #### Inputs
185
-
186
- | Property | Type | Default | Description |
187
- |----------|------|---------|-------------|
188
- | `[allowSelectItemMultiple]` | `boolean` | `undefined` | Cho phép chọn lại item đã chọn |
189
- | `[changeValidUndefinedResetError]` | `boolean` | `undefined` | Reset error khi valid undefined |
190
- | `[classAvatarInclude]` | `string` | `'mr-[8px]'` | Class CSS cho avatar |
191
- | `[classInclude]` | `string` | `undefined` | Class CSS bổ sung cho container |
192
- | `[classIncludeContent]` | `string` | `undefined` | Class CSS cho content wrapper |
193
- | `[classIncludeIcon]` | `string` | `'ml-[8px]'` | Class CSS cho icon mũi tên |
194
- | `[classIncludeTextDisplayWhenNoSelect]` | `string` | `'libs-ui-font-h5r'` | Class CSS cho text placeholder |
195
- | `[convertItemSelected]` | `(item, translate?) => void` | `defaultConvert` | Hàm convert item sau khi chọn để hiển thị label |
196
- | `[disable]` | `boolean` | `undefined` | hiệu hóa dropdown |
197
- | `[disableLabel]` | `boolean` | `undefined` | hiệu hóa label |
198
- | `[fieldGetColorAvatar]` | `string` | `undefined` | Field lấy color cho avatar |
199
- | `[fieldGetIcon]` | `string` | `undefined` | Field lấy icon class từ item |
200
- | `[fieldGetImage]` | `string` | `undefined` | Field lấy URL ảnh từ item |
201
- | `[fieldGetLabel]` | `string` | `undefined` | Field lấy label từ item |
202
- | `[fieldGetTextAvatar]` | `string` | `'username'` | Field lấy text cho avatar |
203
- | `[fieldLabel]` | `string` | `'labelDisplay'` | Field lưu label hiển thị trên dropdown |
204
- | `[flagMouse]` | `IFlagMouse (model)` | `{ isMouseEnter: false, isMouseEnterContent: false }` | Trạng thái mouse |
205
- | `[flagMouseContent]` | `IFlagMouse (model)` | `undefined` | Trạng thái mouse content |
206
- | `[focusInputSearch]` | `boolean` | `true` | Tự động focus input tìm kiếm khi mở |
207
- | `[getLastTextAfterSpace]` | `boolean` | `undefined` | Lấy text cuối sau dấu cách |
208
- | `[getPopoverItemSelected]` | `(item, translate?) => Promise<IPopover>` | `undefined` | Hàm lấy popover config cho item đã chọn |
209
- | `[hasContentUnitRight]` | `boolean` | `undefined` | content bên phải unit |
210
- | `[httpRequestDetailItemById]` | `IHttpRequestConfig` | `undefined` | Config HTTP request để lấy chi tiết item theo key |
211
- | `[ignoreBorderBottom]` | `boolean` | `undefined` | Bỏ border bottom |
212
- | `[ignoreStopPropagationEvent]` | `boolean` | `false` | Bỏ qua stopPropagation |
213
- | `[imageSize]` | `TYPE_SIZE_AVATAR_CONFIG` | `16` | Kích thước avatar |
214
- | `[isNgContent]` | `boolean` | `undefined` | Sử dụng ng-content thay giao diện mặc định |
215
- | `[isSearchOnline]` | `boolean` | `false` | Tìm kiếm online (gọi API) |
216
- | `[labelConfig]` | `ILabel` | `undefined` | Cấu hình label phía trên dropdown |
217
- | `[labelPopoverConfig]` | `IPopoverOverlay` | `undefined` | Config popover cho label |
218
- | `[labelPopoverFullWidth]` | `boolean` | `true` | Popover label full width |
219
- | `[lengthKeys]` | `number (model)` | `0` | Số lượng keys đã chọn |
220
- | `[linkImageError]` | `string` | `undefined` | Link ảnh khi lỗi |
221
- | `[listBackgroundCustom]` | `string` | `undefined` | Background custom cho list |
222
- | `[listButtonsOther]` | `Array<IButton>` | `undefined` | Các button khác trong list |
223
- | `[listClickExactly]` | `boolean` | `undefined` | Click chính xác item |
224
- | `[listConfig]` | `IListConfigItem` | `undefined` | Cấu hình danh sách (type, httpRequestData, template config) |
225
- | `[listConfigHasDivider]` | `boolean` | `true` | Hiển thị divider trong list |
226
- | `[listDividerClassInclude]` | `string` | `undefined` | Class CSS cho divider |
227
- | `[listHasButtonUnSelectOption]` | `boolean` | `undefined` | Hiển thị nút bỏ chọn |
228
- | `[listHiddenInputSearch]` | `boolean` | `undefined` | Ẩn input tìm kiếm |
229
- | `[listIgnoreClassDisableDefaultWhenUseKeysDisableItem]` | `boolean` | `undefined` | Bỏ class disable mặc định khi dùng keysDisable |
230
- | `[listKeysDisable]` | `Array<string>` | `undefined` | Danh sách keys bị disable |
231
- | `[listKeysHidden]` | `Array<string>` | `undefined` | Danh sách keys bị ẩn |
232
- | `[listKeySearch]` | `string` | `undefined` | Key tìm kiếm |
233
- | `[listKeySelected]` | `unknown (model)` | `undefined` | Key item đang được chọn (single select) |
234
- | `[listMaxItemShow]` | `number` | `5` | Số item tối đa hiển thị |
235
- | `[listMultiKeySelected]` | `Array<unknown> (model)` | `undefined` | Danh sách keys đang được chọn (multi select) |
236
- | `[listSearchConfig]` | `IInputSearchConfig` | `{ noBorder: true }` | Cấu hình input tìm kiếm |
237
- | `[listSearchNoDataTemplateRef]` | `TemplateRef<unknown>` | `undefined` | Template khi không dữ liệu tìm kiếm |
238
- | `[listSearchPadding]` | `boolean` | `undefined` | Padding cho search |
239
- | `[onlyEmitDataWhenReset]` | `boolean` | `undefined` | Chỉ emit sự kiện khi reset |
240
- | `[popoverCustomConfig]` | `IPopoverCustomConfig` | `undefined` | Cấu hình popover overlay |
241
- | `[popoverElementRefCustom]` | `HTMLElement` | `undefined` | Element ref custom cho popover |
242
- | `[readonly]` | `boolean` | `undefined` | Chế độ chỉ đọc |
243
- | `[resetKeyWhenSelectAllKey]` | `boolean` | `undefined` | Reset key khi select all |
244
- | `[showBorderError]` | `boolean (model)` | `undefined` | Hiển thị border error |
245
- | `[showError]` | `boolean` | `true` | Hiển thị error message |
246
- | `[tabKeyActive]` | `string (model)` | `undefined` | Key tab đang active |
247
- | `[tabsConfig]` | `Array<IDropdownTabsItem>` | `undefined` | Cấu hình tabs cho dropdown |
248
- | `[textDisplayWhenMultiSelect]` | `string` | `'i18n_selecting_options'` | Text hiển thị khi chọn nhiều |
249
- | `[textDisplayWhenNoSelect]` | `string` | `'i18n_select_information'` | Text hiển thị khi chưa chọn |
250
- | `[typeShape]` | `TYPE_SHAPE_AVATAR` | `'circle'` | Hình dạng avatar |
251
- | `[useXssFilter]` | `boolean` | `false` | Bật XSS filter |
252
- | `[validMaxItemSelected]` | `IValidMaxItemSelected` | `undefined` | Validation số item chọn tối đa |
253
- | `[validRequired]` | `IMessageTranslate` | `undefined` | Validation required |
254
- | `[zIndex]` | `number` | `undefined` | z-index cho popover |
255
-
256
- #### Outputs
257
-
258
- | Property | Type | Description |
259
- |----------|------|-------------|
260
- | `(outChangStageFlagMouse)` | `IFlagMouse` | Emit khi trạng thái mouse thay đổi |
261
- | `(outChangeTabKeyActive)` | `string \| undefined` | Emit khi tab active thay đổi |
262
- | `(outClickButtonOther)` | `IButton` | Emit khi click button khác trong list |
263
- | `(outDataChange)` | `Array<unknown>` | Emit khi dữ liệu list thay đổi |
264
- | `(outFunctionsControl)` | `IDropdownFunctionControlEvent` | Emit functions để điều khiển dropdown |
265
- | `(outSelectKey)` | `IEmitSelectKey \| undefined` | Emit khi chọn item (single select) |
266
- | `(outSelectMultiKey)` | `IEmitMultiKey \| undefined` | Emit khi chọn items (multi select) |
267
- | `(outShowList)` | `boolean` | Emit khi show/hide list |
268
- | `(outValidEvent)` | `boolean` | Emit kết quả validation |
269
-
270
- #### FunctionControl Methods
271
-
272
- | Method | Description |
273
- |--------|-------------|
274
- | `checkIsValid()` | Kiểm tra validation và trả về boolean |
275
- | `getDisable()` | Lấy trạng thái disable |
276
- | `refreshList()` | Refresh lại danh sách dữ liệu |
277
- | `removeList()` | Đóng popup danh sách |
278
- | `reset()` | Reset dropdown về trạng thái ban đầu |
279
- | `resetError()` | Xóa error hiện tại |
280
- | `setError(message)` | Đặt error message |
281
- | `setItemSelectedByKey(id)` | Chọn item theo key (gọi API nếu có httpRequestDetailItemById) |
282
- | `updateLabelItemSelected(label)` | Cập nhật label của item đã chọn |
546
+ ## @Input()
547
+
548
+ | Input | Type | Default | Mô tả | Ví dụ |
549
+ |---|---|---|---|---|
550
+ | `[allowSelectItemMultiple]` | `boolean` | `undefined` | Cho phép chọn lại item đã chọn dù key không đổi | `[allowSelectItemMultiple]="true"` |
551
+ | `[changeValidUndefinedResetError]` | `boolean` | `undefined` | Tự động reset error khi `validRequired` thay đổi về undefined | `[changeValidUndefinedResetError]="true"` |
552
+ | `[classAvatarInclude]` | `string` | `'mr-[8px]'` | Class CSS bổ sung cho avatar hiển thị bên trái label | `[classAvatarInclude]="'mr-[4px]'"` |
553
+ | `[classInclude]` | `string` | `undefined` | Class CSS bổ sung cho wrapper ngoài cùng của dropdown | `[classInclude]="'w-[300px]'"` |
554
+ | `[classIncludeContent]` | `string` | `undefined` | Class CSS bổ sung cho phần trigger content (box hiển thị item đã chọn) | `[classIncludeContent]="'h-[40px]'"` |
555
+ | `[classIncludeIcon]` | `string` | `'ml-[8px]'` | Class CSS bổ sung cho icon mũi tên bên phải | `[classIncludeIcon]="'ml-[4px]'"` |
556
+ | `[classIncludeTextDisplayWhenNoSelect]` | `string` | `'libs-ui-font-h5r'` | Class CSS cho text placeholder khi chưa chọn | `[classIncludeTextDisplayWhenNoSelect]="'libs-ui-font-h5m'"` |
557
+ | `[convertItemSelected]` | `(item: unknown, translate?: TranslateService) => void` | `defaultConvert` | Hàm chuyển đổi item đã chọn để lấy label hiển thị vào field `labelDisplay` | `[convertItemSelected]="convertFn"` |
558
+ | `[disable]` | `boolean` | `undefined` | hiệu hóa dropdown, không cho tương tác | `[disable]="true"` |
559
+ | `[disableLabel]` | `boolean` | `undefined` | hiệu hóa label phía trên dropdown | `[disableLabel]="true"` |
560
+ | `[dropdownTemplateRefNotSearchNoData]` | `TemplateRef<TYPE_TEMPLATE_REF>` | `undefined` | Template custom cho trạng thái "chưa search" hoặc empty state | `[dropdownTemplateRefNotSearchNoData]="myTemplate"` |
561
+ | `[fieldGetColorAvatar]` | `string` | `undefined` | Tên field lấy màu nền cho avatar từ item | `[fieldGetColorAvatar]="'color'"` |
562
+ | `[fieldGetIcon]` | `string` | `undefined` | Tên field lấy class icon từ item để hiển thị bên trái label | `[fieldGetIcon]="'iconClass'"` |
563
+ | `[fieldGetImage]` | `string` | `undefined` | Tên field lấy URL ảnh avatar từ item | `[fieldGetImage]="'avatar_url'"` |
564
+ | `[fieldGetLabel]` | `string` | `undefined` | Tên field lấy label từ item (override mặc định `label`/`name`) | `[fieldGetLabel]="'full_name'"` |
565
+ | `[fieldGetTextAvatar]` | `string` | `'username'` | Tên field lấy text để render avatar chữ | `[fieldGetTextAvatar]="'name'"` |
566
+ | `[fieldLabel]` | `string` | `'labelDisplay'` | Tên field lưu label hiển thị sau khi `convertItemSelected` chạy | `[fieldLabel]="'displayName'"` |
567
+ | `[flagMouse]` | `IFlagMouse` (model) | `{ isMouseEnter: false, isMouseEnterContent: false }` | Two-way binding trạng thái chuột vào/ra dropdown trigger | `[(flagMouse)]="flagMouse"` |
568
+ | `[flagMouseContent]` | `IFlagMouse` (model) | `undefined` | Two-way binding trạng thái chuột vào/ra vùng nội dung popover | `[(flagMouseContent)]="flagMouseContent"` |
569
+ | `[focusInputSearch]` | `boolean` | `true` | Tự động focus vào input tìm kiếm khi mở dropdown | `[focusInputSearch]="false"` |
570
+ | `[getLastTextAfterSpace]` | `boolean` | `undefined` | Lấy phần text sau khoảng trắng cuối cùng để hiển thị lên avatar | `[getLastTextAfterSpace]="true"` |
571
+ | `[getPopoverItemSelected]` | `(item, translate?) => Promise<IPopover \| undefined>` | `undefined` | Hàm async trả về config popover hiển thị khi hover item đã chọn | `[getPopoverItemSelected]="getPopoverFn"` |
572
+ | `[hasContentUnitRight]` | `boolean` | `undefined` | Bỏ border-radius góc phải để ghép với unit bên phải | `[hasContentUnitRight]="true"` |
573
+ | `[httpRequestDetailItemById]` | `IHttpRequestConfig` | `undefined` | Config HTTP request để lazy-load chi tiết item theo key khi chọn | `[httpRequestDetailItemById]="detailConfig"` |
574
+ | `[ignoreBorderBottom]` | `boolean` | `undefined` | Ẩn border bottom của tabs header | `[ignoreBorderBottom]="true"` |
575
+ | `[ignoreStopPropagationEvent]` | `boolean` | `false` | Bỏ qua `stopPropagation` khi click trigger | `[ignoreStopPropagationEvent]="true"` |
576
+ | `[imageSize]` | `TYPE_SIZE_AVATAR_CONFIG` | `16` | Kích thước avatar hiển thị (pixel) | `[imageSize]="24"` |
577
+ | `[isNgContent]` | `boolean` | `undefined` | Dùng `ng-content` làm trigger thay vì giao diện mặc định | `[isNgContent]="true"` |
578
+ | `[isSearchOnline]` | `boolean` | `false` | Gọi API mỗi lần thay đổi từ khóa tìm kiếm (search online) | `[isSearchOnline]="true"` |
579
+ | `[labelConfig]` | `ILabel` | `undefined` | Cấu hình label phía trên dropdown (labelLeft, required, description, buttons...) | `[labelConfig]="{ labelLeft: 'Tên', required: true }"` |
580
+ | `[labelPopoverConfig]` | `IPopoverOverlay` | `undefined` | Config popover tooltip cho label hiển thị item đã chọn | `[labelPopoverConfig]="{ maxWidth: 300 }"` |
581
+ | `[labelPopoverFullWidth]` | `boolean` | `true` | Label popover chiếm full width | `[labelPopoverFullWidth]="false"` |
582
+ | `[lengthKeys]` | `number` (model) | `0` | Two-way binding số lượng key đã chọn | `[(lengthKeys)]="selectedCount"` |
583
+ | `[linkImageError]` | `string` | `undefined` | URL ảnh fallback khi ảnh avatar bị lỗi | `[linkImageError]="'/assets/default.png'"` |
584
+ | `[listBackgroundCustom]` | `string` | `undefined` | Background color custom cho danh sách | `[listBackgroundCustom]="'#f5f5f5'"` |
585
+ | `[listButtonsOther]` | `Array<IButton>` | `undefined` | Các button bổ sung hiển thị ở cuối danh sách | `[listButtonsOther]="extraButtons"` |
586
+ | `[listClickExactly]` | `boolean` | `undefined` | Chỉ phản hồi click chính xác vào item (không phải vùng padding) | `[listClickExactly]="true"` |
587
+ | `[listConfig]` | `IListConfigItem` | `undefined` | **Bắt buộc.** Cấu hình danh sách (type, httpRequestData, template config) | `[listConfig]="myListConfig"` |
588
+ | `[listConfigHasDivider]` | `boolean` | `true` | Hiển thị đường divider trong danh sách | `[listConfigHasDivider]="false"` |
589
+ | `[listDividerClassInclude]` | `string` | `undefined` | Class CSS bổ sung cho đường divider | `[listDividerClassInclude]="'my-[4px]'"` |
590
+ | `[listHasButtonUnSelectOption]` | `boolean` | `auto` | Hiển thị nút "Bỏ chọn" trong danh sách | `[listHasButtonUnSelectOption]="true"` |
591
+ | `[listHiddenInputSearch]` | `boolean` | `undefined` | Ẩn input tìm kiếm | `[listHiddenInputSearch]="true"` |
592
+ | `[listIgnoreClassDisableDefaultWhenUseKeysDisableItem]` | `boolean` | `undefined` | Bỏ class style disable mặc định khi dùng `listKeysDisable` (để tự xử lý styling) | `[listIgnoreClassDisableDefaultWhenUseKeysDisableItem]="true"` |
593
+ | `[listKeysDisable]` | `Array<string>` | `undefined` | Danh sách key của item bị vô hiệu hóa trong list (không cho chọn) | `[listKeysDisable]="['id1', 'id2']"` |
594
+ | `[listKeysHidden]` | `Array<string>` | `undefined` | Danh sách key của item bị ẩn trong list | `[listKeysHidden]="['id3']"` |
595
+ | `[listKeySearch]` | `string` | `undefined` | Tên field dùng để tìm kiếm trong danh sách (mặc định dùng field label) | `[listKeySearch]="'name'"` |
596
+ | `[(listKeySelected)]` | `unknown` (model) | `undefined` | Two-way binding key item đang được chọn (chọn đơn) | `[(listKeySelected)]="selectedId"` |
597
+ | `[listMaxItemShow]` | `number` | `5` | Số item tối đa hiển thị trong danh sách trước khi cuộn (`-1` = không giới hạn) | `[listMaxItemShow]="8"` |
598
+ | `[(listMultiKeySelected)]` | `Array<unknown>` (model) | `undefined` | Two-way binding mảng key đang được chọn (chọn nhiều) | `[(listMultiKeySelected)]="selectedIds"` |
599
+ | `[listSearchConfig]` | `IInputSearchConfig` | `{ noBorder: true }` | Cấu hình input tìm kiếm (placeholder, noBorder...) | `[listSearchConfig]="{ noBorder: true, placeholder: 'Tìm kiếm...' }"` |
600
+ | `[listSearchNoDataTemplateRef]` | `TemplateRef<unknown>` | `undefined` | Template khi tìm kiếm không kết quả | `[listSearchNoDataTemplateRef]="noDataTpl"` |
601
+ | `[listSearchPadding]` | `boolean` | `undefined` | Thêm padding cho phần search | `[listSearchPadding]="true"` |
602
+ | `[onlyEmitDataWhenReset]` | `boolean` | `undefined` | Chỉ emit `outSelectKey`/`outSelectMultiKey` (với undefined) khi gọi `reset()` | `[onlyEmitDataWhenReset]="true"` |
603
+ | `[popoverCustomConfig]` | `IPopoverCustomConfig` | `undefined` | Cấu hình tùy chỉnh cho popover overlay (width, direction, maxHeight...) | `[popoverCustomConfig]="popoverConfig"` |
604
+ | `[popoverElementRefCustom]` | `HTMLElement` | `undefined` | Element HTML tùy chỉnh làm anchor cho popover | `[popoverElementRefCustom]="myEl"` |
605
+ | `[readonly]` | `boolean` | `undefined` | Chế độ chỉ đọc (hiển thị nhưng không cho tương tác) | `[readonly]="true"` |
606
+ | `[resetKeyWhenSelectAllKey]` | `boolean` | `undefined` | Reset key đã chọn khi checkbox "Chọn tất cả" được nhấn | `[resetKeyWhenSelectAllKey]="true"` |
607
+ | `[(showBorderError)]` | `boolean` (model) | `undefined` | Two-way binding hiển thị border màu đỏ (lỗi) | `[(showBorderError)]="hasError"` |
608
+ | `[showError]` | `boolean` | `true` | Hiển thị thông báo lỗi validation dưới dropdown | `[showError]="false"` |
609
+ | `[(tabKeyActive)]` | `string` (model) | `undefined` | Two-way binding key của tab đang active | `[(tabKeyActive)]="activeTab"` |
610
+ | `[tabsConfig]` | `Array<IDropdownTabsItem>` | `undefined` | Cấu hình tabs hiển thị phía trên danh sách | `[tabsConfig]="tabsConfig"` |
611
+ | `[textDisplayWhenMultiSelect]` | `string` | `'i18n_selecting_options'` | Text hiển thị khi chọn nhiều hơn 1 item | `[textDisplayWhenMultiSelect]="'i18n_selected_count'"` |
612
+ | `[textDisplayWhenNoSelect]` | `string` | `'i18n_select_information'` | Text placeholder hiển thị khi chưa chọn item nào | `[textDisplayWhenNoSelect]="'i18n_choose_option'"` |
613
+ | `[typeShape]` | `TYPE_SHAPE_AVATAR` | `'circle'` | Hình dạng của avatar (`'circle'` hoặc `'square'`) | `[typeShape]="'square'"` |
614
+ | `[useXssFilter]` | `boolean` | `false` | Bật XSS filter cho nội dung label hiển thị | `[useXssFilter]="true"` |
615
+ | `[validMaxItemSelected]` | `IValidMaxItemSelected` | `undefined` | Cấu hình validation giới hạn số item chọn tối đa | `[validMaxItemSelected]="{ value: 5, message: 'Tối đa 5 mục' }"` |
616
+ | `[validRequired]` | `IMessageTranslate` | `undefined` | Bật validation bắt buộc chọn; truyền `{}` để dùng message mặc định | `[validRequired]="{ message: 'i18n_required' }"` |
617
+ | `[zIndex]` | `number` | `undefined` | z-index cho popover overlay (mặc định 1000) | `[zIndex]="1050"` |
618
+
619
+ ## @Output()
620
+
621
+ | Output | Type | Mô tả | Handler TS | Binding HTML |
622
+ |---|---|---|---|---|
623
+ | `(outChangStageFlagMouse)` | `IFlagMouse` | Emit khi trạng thái hover chuột vào/ra dropdown thay đổi (merge cả trigger và content) | `handlerChangStageFlagMouse(e: IFlagMouse): void { e; }` | `(outChangStageFlagMouse)="handlerChangStageFlagMouse($event)"` |
624
+ | `(outChangeTabKeyActive)` | `string \| undefined` | Emit key của tab vừa được chọn | `handlerChangeTabKeyActive(e: string \| undefined): void { e; }` | `(outChangeTabKeyActive)="handlerChangeTabKeyActive($event)"` |
625
+ | `(outClickButtonOther)` | `IButton` | Emit khi click vào button bổ sung trong danh sách (`listButtonsOther`) | `handlerClickButtonOther(e: IButton): void { e.stopPropagation(); }` | `(outClickButtonOther)="handlerClickButtonOther($event)"` |
626
+ | `(outDataChange)` | `Array<unknown>` | Emit toàn bộ danh sách hiện tại mỗi khi dữ liệu list thay đổi (sau load/refresh) | `handlerDataChange(e: Array<unknown>): void { e; }` | `(outDataChange)="handlerDataChange($event)"` |
627
+ | `(outFunctionsControl)` | `IDropdownFunctionControlEvent` | Emit object chứa các hàm điều khiển dropdown ngay sau `ngOnInit` | `handlerFunctionsControl(e: IDropdownFunctionControlEvent): void { this.ctrl = e; }` | `(outFunctionsControl)="handlerFunctionsControl($event)"` |
628
+ | `(outSelectKey)` | `IEmitSelectKey \| undefined` | Emit khi chọn 1 item (single select: type text/radio); `undefined` khi bỏ chọn | `handlerSelectKey(e: IEmitSelectKey \| undefined): void { e?.key; }` | `(outSelectKey)="handlerSelectKey($event)"` |
629
+ | `(outSelectMultiKey)` | `IEmitMultiKey \| undefined` | Emit khi chọn/bỏ chọn items (multi select: type checkbox/group); `undefined` khi reset | `handlerSelectMultiKey(e: IEmitMultiKey \| undefined): void { e?.keys; }` | `(outSelectMultiKey)="handlerSelectMultiKey($event)"` |
630
+ | `(outShowList)` | `boolean` | Emit `true` khi mở dropdown, `false` khi đóng | `handlerShowList(e: boolean): void { e; }` | `(outShowList)="handlerShowList($event)"` |
631
+ | `(outValidEvent)` | `boolean` | Emit kết quả validation mỗi khi thay đổi lựa chọn (`true` = hợp lệ) | `handlerValidEvent(e: boolean): void { e; }` | `(outValidEvent)="handlerValidEvent($event)"` |
632
+
633
+ ## FunctionControl Methods
634
+
635
+ `IDropdownFunctionControlEvent` được emit qua `(outFunctionsControl)` ngay sau `ngOnInit`. Lưu vào biến và gọi khi cần:
636
+
637
+ ```typescript
638
+ private dropdownControl: IDropdownFunctionControlEvent | undefined;
639
+
640
+ handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
641
+ this.dropdownControl = event;
642
+ }
643
+ ```
644
+
645
+ | Method | Signature | tả |
646
+ |---|---|---|
647
+ | `checkIsValid()` | `() => Promise<boolean>` | Chạy validation và trả về `true` nếu hợp lệ, `false` nếu lỗi |
648
+ | `getDisable()` | `() => Promise<boolean>` | Lấy trạng thái disable hiện tại |
649
+ | `refreshList()` | `() => Promise<void>` | Reload lại danh sách từ API (gọi `httpRequestData` lại từ đầu) |
650
+ | `removeList()` | `() => Promise<void>` | Đóng popover danh sách nếu đang mở |
651
+ | `reset()` | `() => Promise<void>` | Reset về trạng thái ban đầu: xóa lựa chọn, xóa error |
652
+ | `resetError()` | `() => Promise<void>` | Xóa error message đang hiển thị và border đỏ |
653
+ | `setError(message)` | `(message: string) => Promise<void>` | Hiển thị error message tùy chỉnh (có thể truyền i18n key) |
654
+ | `setItemSelectedByKey(id)` | `(id: unknown) => Promise<void>` | Chọn item theo key, tự gọi API `httpRequestDetailItemById` nếu có để lấy chi tiết |
655
+ | `updateLabelItemSelected(label)` | `(label: string) => Promise<void>` | Cập nhật text hiển thị của item đã chọn mà không cần reload |
283
656
 
284
657
  ## Types & Interfaces
285
658
 
286
659
  ```typescript
660
+ import {
661
+ IEmitSelectKey,
662
+ IEmitMultiKey,
663
+ IPopoverCustomConfig,
664
+ IDropdownTabsItem,
665
+ IValidMaxItemSelected,
666
+ IDropdown,
667
+ IDropdownFunctionControlEvent,
668
+ } from '@libs-ui/components-dropdown';
669
+ ```
670
+
671
+ ```typescript
672
+ // Dữ liệu emit khi chọn 1 item (single select)
287
673
  interface IEmitSelectKey {
288
- key?: unknown;
289
- item?: any;
290
- isClickManual?: boolean;
291
- tabKeyActive?: string;
674
+ key?: unknown; // id / value của item đã chọn
675
+ item?: any; // object item đầy đủ từ danh sách
676
+ isClickManual?: boolean; // true nếu người dùng click trực tiếp
677
+ tabKeyActive?: string; // key tab đang active khi chọn (nếu có tabs)
292
678
  }
293
679
 
680
+ // Dữ liệu emit khi chọn nhiều items (multi select)
294
681
  interface IEmitMultiKey {
295
- keys?: Array<unknown>;
296
- mapKeys?: Array<IEmitSelectKey>;
682
+ keys?: Array<unknown>; // mảng id / value đã chọn
683
+ mapKeys?: Array<IEmitSelectKey>; // mảng chi tiết từng item đã chọn
297
684
  isClickManual?: boolean;
298
685
  tabKeyActive?: string;
299
686
  }
300
687
 
688
+ // Cấu hình tùy chỉnh popover overlay
301
689
  interface IPopoverCustomConfig {
302
- widthByParent?: boolean;
303
- parentBorderWidth?: number;
304
- maxHeight?: number;
305
- maxWidth?: number;
306
- direction?: TYPE_POPOVER_DIRECTION;
307
- ignoreArrow?: boolean;
308
- classInclude?: string;
309
- disable?: boolean;
310
- clickExactly?: boolean;
311
- paddingLeftItem?: boolean;
312
- timerDestroy?: number;
313
- position?: { mode: TYPE_POPOVER_POSITION_MODE; distance: number };
314
- animationConfig?: { time?: number; distance?: number };
315
- width?: number;
316
- classIncludeOverlayBody?: string;
690
+ widthByParent?: boolean; // width bằng element cha
691
+ parentBorderWidth?: number; // bù trừ border của element cha (px)
692
+ maxHeight?: number; // chiều cao tối đa popup (px)
693
+ maxWidth?: number; // chiều rộng tối đa popup (px)
694
+ direction?: TYPE_POPOVER_DIRECTION; // hướng mở: 'bottom' | 'top' | 'left' | 'right'
695
+ ignoreArrow?: boolean; // ẩn mũi tên
696
+ classInclude?: string; // class CSS bổ sung cho container popup
697
+ disable?: boolean; // vô hiệu hóa popover
698
+ clickExactly?: boolean; // chỉ mở khi click chính xác
699
+ paddingLeftItem?: boolean; // thêm padding trái cho item
700
+ timerDestroy?: number; // thời gian trễ trước khi destroy (ms)
701
+ position?: {
702
+ mode: TYPE_POPOVER_POSITION_MODE; // 'start' | 'center' | 'end'
703
+ distance: number; // khoảng cách từ điểm neo (px)
704
+ };
705
+ animationConfig?: {
706
+ time?: number; // thời gian animation (ms)
707
+ distance?: number; // khoảng cách dịch chuyển animation (px)
708
+ };
709
+ width?: number; // chiều rộng cố định (px)
710
+ classIncludeOverlayBody?: string; // class CSS cho overlay body
317
711
  }
318
712
 
713
+ // Cấu hình từng tab trong dropdown có tabs
319
714
  interface IDropdownTabsItem {
320
- key: string;
321
- name: string;
322
- httpRequestData?: IHttpRequestConfig;
715
+ key: string; // định danh duy nhất của tab
716
+ name: string; // tên hiển thị (hỗ trợ i18n key)
717
+ httpRequestData?: IHttpRequestConfig; // config API riêng cho tab này
323
718
  }
324
719
 
720
+ // Validation giới hạn số item được chọn tối đa
325
721
  interface IValidMaxItemSelected extends IMessageTranslate {
326
- value: number;
722
+ value: number; // số lượng tối đa cho phép chọn
327
723
  }
328
724
 
725
+ // Cấu hình dropdown dùng để truyền vào service/helper
329
726
  interface IDropdown {
330
727
  listConfig: IListConfigItem;
331
728
  listBackgroundListCustom?: string;
@@ -338,6 +735,7 @@ interface IDropdown {
338
735
  disable?: boolean;
339
736
  }
340
737
 
738
+ // Interface FunctionControl để điều khiển dropdown từ bên ngoài
341
739
  interface IDropdownFunctionControlEvent {
342
740
  checkIsValid: () => Promise<boolean>;
343
741
  resetError: () => Promise<void>;
@@ -351,62 +749,43 @@ interface IDropdownFunctionControlEvent {
351
749
  }
352
750
  ```
353
751
 
354
- | Type | Mô tả |
355
- |------|--------|
356
- | `IEmitSelectKey` | Dữ liệu emit khi chọn 1 item (single select) |
357
- | `IEmitMultiKey` | Dữ liệu emit khi chọn nhiều items (multi select) |
358
- | `IPopoverCustomConfig` | Cấu hình tùy chỉnh popover overlay |
359
- | `IDropdownTabsItem` | Cấu hình cho từng tab trong dropdown |
360
- | `IValidMaxItemSelected` | Cấu hình validation giới hạn số item được chọn |
361
- | `IDropdown` | Interface cấu hình dropdown |
362
- | `IDropdownFunctionControlEvent` | Interface chứa các methods để điều khiển dropdown từ component cha |
363
-
364
- ## Dependencies
365
-
366
- | Package | Version | Mục đích |
367
- |---------|---------|----------|
368
- | `@libs-ui/components-avatar` | `0.2.355-14` | Hiển thị avatar trong dropdown |
369
- | `@libs-ui/components-buttons-button` | `0.2.355-14` | Button interface |
370
- | `@libs-ui/components-inputs-search` | `0.2.355-14` | Input tìm kiếm |
371
- | `@libs-ui/components-inputs-valid` | `0.2.355-14` | Validation input |
372
- | `@libs-ui/components-label` | `0.2.355-14` | Label component |
373
- | `@libs-ui/components-list` | `0.2.355-14` | Danh sách hiển thị |
374
- | `@libs-ui/components-popover` | `0.2.355-14` | Popover overlay |
375
- | `@libs-ui/pipes-security-trust` | `0.2.355-14` | Security trust pipe |
376
- | `@libs-ui/services-http-request` | `0.2.355-14` | HTTP request service |
377
- | `@libs-ui/utils` | `0.2.355-14` | Utilities |
378
- | `@ngx-translate/core` | `^15.0.0` | Đa ngôn ngữ |
379
-
380
- ## Công nghệ
381
-
382
- | Technology | Version | Purpose |
383
- |------------|---------|---------|
384
- | Angular | 18+ | Framework |
385
- | Angular Signals | - | State management |
386
- | TailwindCSS | 3.x | Styling |
387
- | OnPush | - | Change Detection |
752
+ ## Sub-Component: libs_ui-components-dropdown-tabs
388
753
 
389
- ## Demo
754
+ Component tabs nội bộ, được sử dụng tự động khi truyền `[tabsConfig]` vào dropdown chính. Không cần import riêng.
390
755
 
391
- ```bash
392
- npx nx serve core-ui
393
- ```
756
+ | Input | Type | Default | Mô tả | Ví dụ |
757
+ |---|---|---|---|---|
758
+ | `[tabsConfig]` | `Array<IDropdownTabsItem>` | `undefined` | Danh sách cấu hình tab | `[tabsConfig]="tabs"` |
759
+ | `[(tabKeyActive)]` | `string` (model) | `undefined` | Two-way binding key tab đang active | `[(tabKeyActive)]="activeKey"` |
760
+ | `[ignoreBorderBottom]` | `boolean` | `undefined` | Ẩn border bottom của tabs bar | `[ignoreBorderBottom]="true"` |
761
+ | `[disable]` | `boolean` | `undefined` | Vô hiệu hóa toàn bộ tabs | `[disable]="true"` |
394
762
 
395
- Truy cập: http://localhost:4500/dropdown
763
+ | Output | Type | Mô tả |
764
+ |---|---|---|
765
+ | `(outChange)` | `void` | Emit mỗi khi chọn tab mới |
396
766
 
397
- ## Unit Tests
767
+ ## Lưu ý quan trọng
398
768
 
399
- ```bash
400
- # Chạy tests
401
- npx nx test components-dropdown
769
+ ⚠️ **Bắt buộc `[listConfig]`**: Dropdown không hoạt động nếu thiếu `[listConfig]`. Đây là input quan trọng nhất xác định type hiển thị và nguồn dữ liệu.
402
770
 
403
- # Coverage
404
- npx nx test components-dropdown --coverage
771
+ ⚠️ **Chế độ chọn đơn vs nhiều**: Dùng `[(listKeySelected)]` + `(outSelectKey)` cho type `text`/`radio`. Dùng `[(listMultiKeySelected)]` + `(outSelectMultiKey)` cho type `checkbox`/`group`.
405
772
 
406
- # Watch mode
407
- npx nx test components-dropdown --watch
408
- ```
773
+ ⚠️ **`convertItemSelected` là bắt buộc để hiển thị đúng label**: Hàm này cần gán giá trị vào field `labelDisplay` (hoặc field được chỉ định bởi `[fieldLabel]`) của item. Thiếu hàm này dropdown sẽ hiển thị trắng sau khi chọn.
409
774
 
410
- ## License
775
+ ⚠️ **Truyền key có sẵn (`listKeySelected`/`listMultiKeySelected`) BẮT BUỘC kèm `[httpRequestDetailItemById]`** (lỗi phổ biến nhất): Khi bạn truyền key đã chọn từ server (vd: `[(listKeySelected)]="selectedId"`) mà dropdown CHƯA load list, component chỉ có **key** chứ không có **item** tương ứng để dựng label → trigger sẽ **hiển thị trắng**. Phải cấu hình `[httpRequestDetailItemById]` để dropdown tự gọi API lấy chi tiết item theo key. Nếu thiếu, component sẽ in `console.warn` cảnh báo. Xem mục _"10. Dropdown lazy-load chi tiết item theo key"_.
411
776
 
412
- MIT
777
+ ⚠️ **FunctionControl phát sinh sau ngOnInit**: `(outFunctionsControl)` emit 1 lần duy nhất trong `ngOnInit`. Lưu tham chiếu vào biến class để dùng sau.
778
+
779
+ ⚠️ **`listKeysDisable` không dùng kèm `configCheckboxCheckAll`**: Khi listConfig có cấu hình `configCheckboxCheckAll`, không được dùng `[listKeysDisable]` vì sẽ gây lỗi logic chọn tất cả.
780
+
781
+ ⚠️ **`listKeysHidden` không dùng kèm `configCheckboxCheckAll`**: Tương tự `listKeysDisable`, tránh dùng kết hợp với `configCheckboxCheckAll`.
782
+
783
+ ⚠️ **XSS Safety**: Khi `getValue`/`getLabelItem` trả về HTML có thể chứa dữ liệu từ người dùng, BẮT BUỘC bọc trong `escapeHtml()` từ `@libs-ui/utils`.
784
+
785
+ ## Demo
786
+
787
+ ```bash
788
+ npx nx serve core-ui
789
+ ```
790
+
791
+ Truy cập: http://localhost:4500/dropdown