@libs-ui/components-dropdown
v0.2.357-9
Published
> 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.
Readme
@libs-ui/components-dropdown
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.
Giới thiệu
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.
Tính năng
- ✅ Nhiều chế độ hiển thị:
text,radio,checkbox,group(tree, JSON tree, personalize) - ✅ Tìm kiếm online (gọi API) và offline (client-side) tích hợp sẵn
- ✅ Validation: required, giới hạn số lượng item được chọn tối đa
- ✅ Điều khiển từ bên ngoài qua
IDropdownFunctionControlEvent(reset, refresh, setError, setItemSelectedByKey...) - ✅ Hỗ trợ tabs để phân loại dữ liệu theo nhóm
- ✅ Tự động load và auto-select item đầu tiên hoặc toàn bộ (autoSelectFirstItem / autoSelectAllItem)
- ✅ Hiển thị avatar, icon, image cho từng item trong danh sách
- ✅ Custom content qua
ng-content([isNgContent]="true") - ✅ Tùy chỉnh popover overlay (hướng, width, z-index, animation...)
- ✅ Lazy-load chi tiết item theo key (
httpRequestDetailItemById) - ✅ OnPush Change Detection + Angular Signals
Khi nào sử dụng
- Chọn một hoặc nhiều giá trị từ danh sách dữ liệu API
- Dropdown với tìm kiếm online/offline
- Chọn dữ liệu dạng nhóm (group), cây (tree), JSON tree nested nhiều cấp
- Cần validation (required, max items)
- Cần điều khiển dropdown từ component cha (reset, refresh, check valid, set error)
- Hiển thị dropdown với tabs để phân loại dữ liệu theo nguồn khác nhau
- Dropdown với custom trigger (ng-content) thay vì giao diện mặc định
Cài đặt
npm install @libs-ui/components-dropdownImport
import {
LibsUiComponentsDropdownComponent,
IDropdownFunctionControlEvent,
IEmitSelectKey,
IEmitMultiKey,
IPopoverCustomConfig,
IDropdownTabsItem,
IValidMaxItemSelected,
IDropdown,
} from '@libs-ui/components-dropdown';
@Component({
standalone: true,
imports: [LibsUiComponentsDropdownComponent],
})
export class MyComponent {}Ví dụ sử dụng
1. Basic — Dropdown chọn đơn (type: text)
import { Component, signal } from '@angular/core';
import { LibsUiComponentsDropdownComponent, IEmitSelectKey } from '@libs-ui/components-dropdown';
import { IListConfigItem } from '@libs-ui/components-list';
import { IHttpRequestConfig } from '@libs-ui/services-http-request';
import { escapeHtml, get, set, UtilsHttpParamsRequest } from '@libs-ui/utils';
@Component({
selector: 'app-example-basic',
standalone: true,
imports: [LibsUiComponentsDropdownComponent],
template: `
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn nhân viên', required: true }"
[listConfig]="listConfig"
[listSearchConfig]="{ noBorder: true }"
[listMaxItemShow]="5"
[convertItemSelected]="convertItemSelected"
[validRequired]="{}"
(outSelectKey)="handlerSelectKey($event)"
(outFunctionsControl)="handlerFunctionsControl($event)"
/>
`,
})
export class ExampleBasicComponent {
private dropdownControl: IDropdownFunctionControlEvent | undefined;
readonly listConfig: IListConfigItem = {
type: 'text',
httpRequestData: signal<IHttpRequestConfig>({
objectInstance: myApiService,
functionName: 'getList',
argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
}),
configTemplateText: signal({
fieldKey: 'id',
notUseVirtualScroll: true,
getValue: (item: { name: string }) => escapeHtml(item.name),
}),
};
readonly convertItemSelected = (item: unknown): void => {
if (!item) return;
set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
};
handlerSelectKey(event: IEmitSelectKey | undefined): void {
event?.key; // id item đã chọn
event?.item; // object item đầy đủ
}
handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
this.dropdownControl = event;
}
async resetSelection(): Promise<void> {
await this.dropdownControl?.reset();
}
}2. Dropdown chọn nhiều (type: checkbox)
import { Component, signal } from '@angular/core';
import { LibsUiComponentsDropdownComponent, IEmitMultiKey } from '@libs-ui/components-dropdown';
import { IListConfigItem } from '@libs-ui/components-list';
import { IHttpRequestConfig } from '@libs-ui/services-http-request';
import { escapeHtml, get, set, UtilsHttpParamsRequest } from '@libs-ui/utils';
@Component({
selector: 'app-example-checkbox',
standalone: true,
imports: [LibsUiComponentsDropdownComponent],
template: `
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn nhiều nhãn', required: true }"
[listConfig]="checkboxConfig"
[listSearchConfig]="{ noBorder: true }"
[listMaxItemShow]="5"
[(listMultiKeySelected)]="selectedKeys"
[convertItemSelected]="convertItemSelected"
[validRequired]="{}"
[validMaxItemSelected]="{ value: 3, message: 'Chỉ được chọn tối đa 3 mục' }"
(outSelectMultiKey)="handlerSelectMultiKey($event)"
/>
`,
})
export class ExampleCheckboxComponent {
selectedKeys = signal<string[]>([]);
readonly checkboxConfig: IListConfigItem = {
type: 'checkbox',
httpRequestData: signal<IHttpRequestConfig>({
objectInstance: myApiService,
functionName: 'getList',
argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
}),
autoSelectFirstItem: false,
configTemplateCheckbox: signal({
fieldKey: 'id',
configButtonSelectAndUndSelectItem: signal({}),
getValue: (item: { name: string }) => escapeHtml(item.name),
}),
};
readonly convertItemSelected = (item: unknown): void => {
if (!item) return;
set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
};
handlerSelectMultiKey(event: IEmitMultiKey | undefined): void {
event?.keys; // mảng id đã chọn
event?.mapKeys; // mảng { key, item } đầy đủ
}
}3. Dropdown dạng nhóm — Group Checkbox
import { Component, signal } from '@angular/core';
import { LibsUiComponentsDropdownComponent, IEmitMultiKey } from '@libs-ui/components-dropdown';
import { IListConfigItem } from '@libs-ui/components-list';
import { IHttpRequestConfig, returnListObject } from '@libs-ui/services-http-request';
import { escapeHtml, get, set } from '@libs-ui/utils';
@Component({
selector: 'app-example-group',
standalone: true,
imports: [LibsUiComponentsDropdownComponent],
template: `
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn theo nhóm', required: true }"
[listConfig]="groupConfig"
[listMaxItemShow]="5"
[convertItemSelected]="convertItemSelected"
(outSelectMultiKey)="handlerSelectMultiKey($event)"
/>
`,
})
export class ExampleGroupComponent {
private readonly groupDataService = returnListObject([
{
id: 'nhom_a',
name: 'Nhóm A',
items: [
{ id: 'a1', name: 'Mục A1' },
{ id: 'a2', name: 'Mục A2' },
],
},
{
id: 'nhom_b',
name: 'Nhóm B',
items: [{ id: 'b1', name: 'Mục B1' }],
},
]);
readonly groupConfig: IListConfigItem = {
type: 'group',
httpRequestData: signal<IHttpRequestConfig>({
objectInstance: this.groupDataService,
functionName: 'list',
argumentsValue: [],
}),
configTemplateGroup: signal({
fieldKey: 'id',
fieldGetItems: 'items',
getLabelGroup: (group: { name: string }) => escapeHtml(group.name),
getMaxLevelGroup: () => 2,
getLabelItem: (item: { name: string }) => escapeHtml(item.name),
iconExpand: 'right',
isViewRadio: false,
}),
};
readonly convertItemSelected = (item: unknown): void => {
if (!item) return;
set(item as Record<string, unknown>, 'labelDisplay', escapeHtml(get(item as Record<string, string>, 'name') || ''));
};
handlerSelectMultiKey(event: IEmitMultiKey | undefined): void {
event?.keys;
}
}4. Dropdown Group Radio — chọn đơn trong nhóm
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn một mục trong nhóm', required: true }"
[listConfig]="groupRadioConfig"
[listMaxItemShow]="5"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
/>readonly groupRadioConfig: IListConfigItem = {
type: 'group',
httpRequestData: signal<IHttpRequestConfig>(groupHttpConfig),
configTemplateGroup: signal({
fieldKey: 'id',
fieldGetItems: 'items',
getLabelGroup: (group: { name: string }) => escapeHtml(group.name),
getMaxLevelGroup: () => 2,
getLabelItem: (item: { name: string }) => escapeHtml(item.name),
iconExpand: 'right',
isViewRadio: true, // chỉ khác group checkbox ở dòng này
}),
};5. Dropdown Radio
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn loại khách hàng', required: true }"
[listConfig]="radioConfig"
[listMaxItemShow]="5"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
/>readonly radioConfig: IListConfigItem = {
type: 'radio',
httpRequestData: signal<IHttpRequestConfig>({
objectInstance: myApiService,
functionName: 'getList',
argumentsValue: [new UtilsHttpParamsRequest({ fromObject: { page: 1, per_page: 20 } })],
}),
configTemplateRadio: signal({
fieldKey: 'id',
getValue: (item: { name: string }) => escapeHtml(item.name),
}),
};6. Dropdown với FunctionControl — điều khiển từ bên ngoài
<div class="flex gap-[8px] mb-[16px]">
<button (click)="handlerReset()">Reset</button>
<button (click)="handlerCheckValid()">Check Valid</button>
<button (click)="handlerRefresh()">Refresh</button>
<button (click)="handlerSetError()">Set Error</button>
</div>
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Dropdown có điều khiển', required: true }"
[listConfig]="listConfig"
[listMaxItemShow]="5"
[validRequired]="{}"
(outFunctionsControl)="handlerFunctionsControl($event)"
(outValidEvent)="handlerValidEvent($event)"
/>import { IDropdownFunctionControlEvent, IEmitSelectKey } from '@libs-ui/components-dropdown';
private dropdownControl: IDropdownFunctionControlEvent | undefined;
handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
this.dropdownControl = event;
}
handlerValidEvent(isValid: boolean): void {
// true khi hợp lệ, false khi có lỗi validation
}
async handlerReset(): Promise<void> {
await this.dropdownControl?.reset();
}
async handlerCheckValid(): Promise<void> {
const isValid = await this.dropdownControl?.checkIsValid();
// isValid: true/false
}
async handlerRefresh(): Promise<void> {
await this.dropdownControl?.refreshList();
}
async handlerSetError(): Promise<void> {
await this.dropdownControl?.setError?.('i18n_error_message');
}
async handlerSetItemByKey(): Promise<void> {
await this.dropdownControl?.setItemSelectedByKey('abc-123');
}
async handlerUpdateLabel(): Promise<void> {
await this.dropdownControl?.updateLabelItemSelected('Tên hiển thị mới');
}7. Dropdown với Tabs phân loại dữ liệu
import { IDropdownTabsItem } from '@libs-ui/components-dropdown';
readonly tabsConfig: IDropdownTabsItem[] = [
{
key: 'all',
name: 'Tất cả',
httpRequestData: {
objectInstance: allApiService,
functionName: 'getList',
argumentsValue: [],
},
},
{
key: 'active',
name: 'Hoạt động',
httpRequestData: {
objectInstance: activeApiService,
functionName: 'getList',
argumentsValue: [],
},
},
];<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn với tab phân loại', required: true }"
[listConfig]="listConfig"
[tabsConfig]="tabsConfig"
[(tabKeyActive)]="activeTabKey"
[listMaxItemShow]="5"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
(outChangeTabKeyActive)="handlerChangeTab($event)"
/>protected activeTabKey = signal<string>('all');
handlerChangeTab(key: string | undefined): void {
// key: tab vừa được chọn
}8. Dropdown trạng thái: Disable / Readonly / Validation
<!-- Disable -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Trạng thái disable' }"
[listConfig]="listConfig"
[disable]="true"
/>
<!-- Readonly -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Trạng thái readonly' }"
[listConfig]="listConfig"
[readonly]="true"
[(listKeySelected)]="selectedKey"
/>
<!-- Validation required -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Bắt buộc chọn', required: true }"
[listConfig]="listConfig"
[validRequired]="{ message: 'i18n_field_required' }"
[showError]="true"
/>
<!-- Validation max items -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Chọn tối đa 3', required: true }"
[listConfig]="checkboxConfig"
[validMaxItemSelected]="{ value: 3, message: 'Chỉ được chọn tối đa {value} mục', interpolateParams: { value: 3 } }"
/>9. Dropdown với tìm kiếm online (Search Online)
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Tìm kiếm online', required: true }"
[listConfig]="listConfig"
[isSearchOnline]="true"
[listSearchConfig]="{ noBorder: true, placeholder: 'Gõ keyword để tìm kiếm...' }"
[listMaxItemShow]="8"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
/>10. Dropdown lazy-load chi tiết item theo key
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ị.
🔴 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.
<!-- ❌ SAI — có key nhưng thiếu httpRequestDetailItemById → label trắng -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Thiếu detail config' }"
[listConfig]="listConfig"
[(listKeySelected)]="selectedKey"
[convertItemSelected]="convertItemSelected"
/><!-- ✅ ĐÚNG -->
<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Dropdown lazy-load detail' }"
[listConfig]="listConfig"
[(listKeySelected)]="selectedKey"
[httpRequestDetailItemById]="httpRequestDetailConfig"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
/>readonly httpRequestDetailConfig: IHttpRequestConfig = {
objectInstance: detailApiService,
functionName: 'getDetailByKey',
argumentsValue: [],
guideAutoUpdateArgumentsValue: {
paging: {},
detailById: {
fieldGetValue: '',
fieldUpdate: '[0]',
},
},
};
selectedKey = signal<string>('item-id-123'); // sẽ tự gọi API để load tên hiển thị11. Dropdown không load list trước khi search
Cơ chế "chưa search thì chưa load" được bật qua ignoreShowDataWhenNotSearch: true bên trong listConfig (thuộc IListConfigItem), không phải một input riêng của dropdown.
readonly listConfigNotSearch: IListConfigItem = {
type: 'text',
httpRequestData: signal<IHttpRequestConfig>({
objectInstance: new UserService(),
functionName: 'list',
argumentsValue: [],
}),
ignoreShowDataWhenNotSearch: true, // không load list khi vừa mở; chỉ load khi có keyword
configTemplateText: signal({ fieldKey: 'id', getValue: (item) => item.name }),
};<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Gõ keyword để bắt đầu load' }"
[listConfig]="listConfigNotSearch"
[isSearchOnline]="true"
[listHiddenInputSearch]="false"
[listSearchConfig]="{ noBorder: true, placeholder: 'Nhập để tìm kiếm...' }"
/>12. Dropdown với Custom Popover Config
import { IPopoverCustomConfig } from '@libs-ui/components-dropdown';
readonly popoverConfig: IPopoverCustomConfig = {
widthByParent: false,
maxWidth: 500,
maxHeight: 400,
direction: 'bottom',
ignoreArrow: true,
position: { mode: 'start', distance: 0 },
animationConfig: { time: 200, distance: 8 },
};<libs_ui-components-dropdown
[labelConfig]="{ labelLeft: 'Dropdown custom popover' }"
[listConfig]="listConfig"
[popoverCustomConfig]="popoverConfig"
[zIndex]="1050"
[convertItemSelected]="convertItemSelected"
(outSelectKey)="handlerSelectKey($event)"
/>@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [allowSelectItemMultiple] | boolean | undefined | Cho phép chọn lại item đã chọn dù key không đổi | [allowSelectItemMultiple]="true" |
| [changeValidUndefinedResetError] | boolean | undefined | Tự động reset error khi validRequired thay đổi về undefined | [changeValidUndefinedResetError]="true" |
| [classAvatarInclude] | string | 'mr-[8px]' | Class CSS bổ sung cho avatar hiển thị bên trái label | [classAvatarInclude]="'mr-[4px]'" |
| [classInclude] | string | undefined | Class CSS bổ sung cho wrapper ngoài cùng của dropdown | [classInclude]="'w-[300px]'" |
| [classIncludeContent] | string | undefined | Class CSS bổ sung cho phần trigger content (box hiển thị item đã chọn) | [classIncludeContent]="'h-[40px]'" |
| [classIncludeIcon] | string | 'ml-[8px]' | Class CSS bổ sung cho icon mũi tên bên phải | [classIncludeIcon]="'ml-[4px]'" |
| [classIncludeTextDisplayWhenNoSelect] | string | 'libs-ui-font-h5r' | Class CSS cho text placeholder khi chưa chọn | [classIncludeTextDisplayWhenNoSelect]="'libs-ui-font-h5m'" |
| [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" |
| [disable] | boolean | undefined | Vô hiệu hóa dropdown, không cho tương tác | [disable]="true" |
| [disableLabel] | boolean | undefined | Vô hiệu hóa label phía trên dropdown | [disableLabel]="true" |
| [dropdownTemplateRefNotSearchNoData] | TemplateRef<TYPE_TEMPLATE_REF> | undefined | Template custom cho trạng thái "chưa search" hoặc empty state | [dropdownTemplateRefNotSearchNoData]="myTemplate" |
| [fieldGetColorAvatar] | string | undefined | Tên field lấy màu nền cho avatar từ item | [fieldGetColorAvatar]="'color'" |
| [fieldGetIcon] | string | undefined | Tên field lấy class icon từ item để hiển thị bên trái label | [fieldGetIcon]="'iconClass'" |
| [fieldGetImage] | string | undefined | Tên field lấy URL ảnh avatar từ item | [fieldGetImage]="'avatar_url'" |
| [fieldGetLabel] | string | undefined | Tên field lấy label từ item (override mặc định label/name) | [fieldGetLabel]="'full_name'" |
| [fieldGetTextAvatar] | string | 'username' | Tên field lấy text để render avatar chữ | [fieldGetTextAvatar]="'name'" |
| [fieldLabel] | string | 'labelDisplay' | Tên field lưu label hiển thị sau khi convertItemSelected chạy | [fieldLabel]="'displayName'" |
| [flagMouse] | IFlagMouse (model) | { isMouseEnter: false, isMouseEnterContent: false } | Two-way binding trạng thái chuột vào/ra dropdown trigger | [(flagMouse)]="flagMouse" |
| [flagMouseContent] | IFlagMouse (model) | undefined | Two-way binding trạng thái chuột vào/ra vùng nội dung popover | [(flagMouseContent)]="flagMouseContent" |
| [focusInputSearch] | boolean | true | Tự động focus vào input tìm kiếm khi mở dropdown | [focusInputSearch]="false" |
| [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" |
| [getPopoverItemSelected] | (item, translate?) => Promise<IPopover \| undefined> | undefined | Hàm async trả về config popover hiển thị khi hover item đã chọn | [getPopoverItemSelected]="getPopoverFn" |
| [hasContentUnitRight] | boolean | undefined | Bỏ border-radius góc phải để ghép với unit bên phải | [hasContentUnitRight]="true" |
| [httpRequestDetailItemById] | IHttpRequestConfig | undefined | Config HTTP request để lazy-load chi tiết item theo key khi chọn | [httpRequestDetailItemById]="detailConfig" |
| [ignoreBorderBottom] | boolean | undefined | Ẩn border bottom của tabs header | [ignoreBorderBottom]="true" |
| [ignoreStopPropagationEvent] | boolean | false | Bỏ qua stopPropagation khi click trigger | [ignoreStopPropagationEvent]="true" |
| [imageSize] | TYPE_SIZE_AVATAR_CONFIG | 16 | Kích thước avatar hiển thị (pixel) | [imageSize]="24" |
| [isNgContent] | boolean | undefined | Dùng ng-content làm trigger thay vì giao diện mặc định | [isNgContent]="true" |
| [isSearchOnline] | boolean | false | Gọi API mỗi lần thay đổi từ khóa tìm kiếm (search online) | [isSearchOnline]="true" |
| [labelConfig] | ILabel | undefined | Cấu hình label phía trên dropdown (labelLeft, required, description, buttons...) | [labelConfig]="{ labelLeft: 'Tên', required: true }" |
| [labelPopoverConfig] | IPopoverOverlay | undefined | Config popover tooltip cho label hiển thị item đã chọn | [labelPopoverConfig]="{ maxWidth: 300 }" |
| [labelPopoverFullWidth] | boolean | true | Label popover chiếm full width | [labelPopoverFullWidth]="false" |
| [lengthKeys] | number (model) | 0 | Two-way binding số lượng key đã chọn | [(lengthKeys)]="selectedCount" |
| [linkImageError] | string | undefined | URL ảnh fallback khi ảnh avatar bị lỗi | [linkImageError]="'/assets/default.png'" |
| [listBackgroundCustom] | string | undefined | Background color custom cho danh sách | [listBackgroundCustom]="'#f5f5f5'" |
| [listButtonsOther] | Array<IButton> | undefined | Các button bổ sung hiển thị ở cuối danh sách | [listButtonsOther]="extraButtons" |
| [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" |
| [listConfig] | IListConfigItem | undefined | Bắt buộc. Cấu hình danh sách (type, httpRequestData, template config) | [listConfig]="myListConfig" |
| [listConfigHasDivider] | boolean | true | Hiển thị đường divider trong danh sách | [listConfigHasDivider]="false" |
| [listDividerClassInclude] | string | undefined | Class CSS bổ sung cho đường divider | [listDividerClassInclude]="'my-[4px]'" |
| [listHasButtonUnSelectOption] | boolean | auto | Hiển thị nút "Bỏ chọn" trong danh sách | [listHasButtonUnSelectOption]="true" |
| [listHiddenInputSearch] | boolean | undefined | Ẩn input tìm kiếm | [listHiddenInputSearch]="true" |
| [listIgnoreClassDisableDefaultWhenUseKeysDisableItem] | boolean | undefined | Bỏ class style disable mặc định khi dùng listKeysDisable (để tự xử lý styling) | [listIgnoreClassDisableDefaultWhenUseKeysDisableItem]="true" |
| [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']" |
| [listKeysHidden] | Array<string> | undefined | Danh sách key của item bị ẩn trong list | [listKeysHidden]="['id3']" |
| [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'" |
| [(listKeySelected)] | unknown (model) | undefined | Two-way binding key item đang được chọn (chọn đơn) | [(listKeySelected)]="selectedId" |
| [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" |
| [(listMultiKeySelected)] | Array<unknown> (model) | undefined | Two-way binding mảng key đang được chọn (chọn nhiều) | [(listMultiKeySelected)]="selectedIds" |
| [listSearchConfig] | IInputSearchConfig | { noBorder: true } | Cấu hình input tìm kiếm (placeholder, noBorder...) | [listSearchConfig]="{ noBorder: true, placeholder: 'Tìm kiếm...' }" |
| [listSearchNoDataTemplateRef] | TemplateRef<unknown> | undefined | Template khi tìm kiếm không có kết quả | [listSearchNoDataTemplateRef]="noDataTpl" |
| [listSearchPadding] | boolean | undefined | Thêm padding cho phần search | [listSearchPadding]="true" |
| [onlyEmitDataWhenReset] | boolean | undefined | Chỉ emit outSelectKey/outSelectMultiKey (với undefined) khi gọi reset() | [onlyEmitDataWhenReset]="true" |
| [popoverCustomConfig] | IPopoverCustomConfig | undefined | Cấu hình tùy chỉnh cho popover overlay (width, direction, maxHeight...) | [popoverCustomConfig]="popoverConfig" |
| [popoverElementRefCustom] | HTMLElement | undefined | Element HTML tùy chỉnh làm anchor cho popover | [popoverElementRefCustom]="myEl" |
| [readonly] | boolean | undefined | Chế độ chỉ đọc (hiển thị nhưng không cho tương tác) | [readonly]="true" |
| [resetKeyWhenSelectAllKey] | boolean | undefined | Reset key đã chọn khi checkbox "Chọn tất cả" được nhấn | [resetKeyWhenSelectAllKey]="true" |
| [(showBorderError)] | boolean (model) | undefined | Two-way binding hiển thị border màu đỏ (lỗi) | [(showBorderError)]="hasError" |
| [showError] | boolean | true | Hiển thị thông báo lỗi validation dưới dropdown | [showError]="false" |
| [(tabKeyActive)] | string (model) | undefined | Two-way binding key của tab đang active | [(tabKeyActive)]="activeTab" |
| [tabsConfig] | Array<IDropdownTabsItem> | undefined | Cấu hình tabs hiển thị phía trên danh sách | [tabsConfig]="tabsConfig" |
| [textDisplayWhenMultiSelect] | string | 'i18n_selecting_options' | Text hiển thị khi chọn nhiều hơn 1 item | [textDisplayWhenMultiSelect]="'i18n_selected_count'" |
| [textDisplayWhenNoSelect] | string | 'i18n_select_information' | Text placeholder hiển thị khi chưa chọn item nào | [textDisplayWhenNoSelect]="'i18n_choose_option'" |
| [typeShape] | TYPE_SHAPE_AVATAR | 'circle' | Hình dạng của avatar ('circle' hoặc 'square') | [typeShape]="'square'" |
| [useXssFilter] | boolean | false | Bật XSS filter cho nội dung label hiển thị | [useXssFilter]="true" |
| [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' }" |
| [validRequired] | IMessageTranslate | undefined | Bật validation bắt buộc chọn; truyền {} để dùng message mặc định | [validRequired]="{ message: 'i18n_required' }" |
| [zIndex] | number | undefined | z-index cho popover overlay (mặc định 1000) | [zIndex]="1050" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (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)" |
| (outChangeTabKeyActive) | string \| undefined | Emit key của tab vừa được chọn | handlerChangeTabKeyActive(e: string \| undefined): void { e; } | (outChangeTabKeyActive)="handlerChangeTabKeyActive($event)" |
| (outClickButtonOther) | IButton | Emit khi click vào button bổ sung trong danh sách (listButtonsOther) | handlerClickButtonOther(e: IButton): void { e.stopPropagation(); } | (outClickButtonOther)="handlerClickButtonOther($event)" |
| (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)" |
| (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)" |
| (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)" |
| (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)" |
| (outShowList) | boolean | Emit true khi mở dropdown, false khi đóng | handlerShowList(e: boolean): void { e; } | (outShowList)="handlerShowList($event)" |
| (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)" |
FunctionControl Methods
IDropdownFunctionControlEvent được emit qua (outFunctionsControl) ngay sau ngOnInit. Lưu vào biến và gọi khi cần:
private dropdownControl: IDropdownFunctionControlEvent | undefined;
handlerFunctionsControl(event: IDropdownFunctionControlEvent): void {
this.dropdownControl = event;
}| Method | Signature | Mô tả |
|---|---|---|
| checkIsValid() | () => Promise<boolean> | Chạy validation và trả về true nếu hợp lệ, false nếu có lỗi |
| getDisable() | () => Promise<boolean> | Lấy trạng thái disable hiện tại |
| refreshList() | () => Promise<void> | Reload lại danh sách từ API (gọi httpRequestData lại từ đầu) |
| removeList() | () => Promise<void> | Đóng popover danh sách nếu đang mở |
| reset() | () => Promise<void> | Reset về trạng thái ban đầu: xóa lựa chọn, xóa error |
| resetError() | () => Promise<void> | Xóa error message đang hiển thị và border đỏ |
| setError(message) | (message: string) => Promise<void> | Hiển thị error message tùy chỉnh (có thể truyền i18n key) |
| setItemSelectedByKey(id) | (id: unknown) => Promise<void> | Chọn item theo key, tự gọi API httpRequestDetailItemById nếu có để lấy chi tiết |
| setSelectedKey(key, options?) | (key: unknown, options?: { reset?: boolean }) => Promise<void> | Set key đã chọn theo chương trình (single select); options.reset=true sẽ reset trước khi set |
| setSelectedMultiKey(keys, options?) | (keys: unknown[], options?: { reset?: boolean }) => Promise<void> | Set mảng key đã chọn theo chương trình (multi select); options.reset=true sẽ reset trước khi set |
| 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 |
Types & Interfaces
import {
IEmitSelectKey,
IEmitMultiKey,
IPopoverCustomConfig,
IDropdownTabsItem,
IValidMaxItemSelected,
IDropdown,
IDropdownFunctionControlEvent,
} from '@libs-ui/components-dropdown';// Dữ liệu emit khi chọn 1 item (single select)
interface IEmitSelectKey {
key?: unknown; // id / value của item đã chọn
item?: any; // object item đầy đủ từ danh sách
isClickManual?: boolean; // true nếu người dùng click trực tiếp
tabKeyActive?: string; // key tab đang active khi chọn (nếu có tabs)
}
// Dữ liệu emit khi chọn nhiều items (multi select)
interface IEmitMultiKey {
keys?: Array<unknown>; // mảng id / value đã chọn
mapKeys?: Array<IEmitSelectKey>; // mảng chi tiết từng item đã chọn
isClickManual?: boolean;
tabKeyActive?: string;
}
// Cấu hình tùy chỉnh popover overlay
interface IPopoverCustomConfig {
widthByParent?: boolean; // width bằng element cha
parentBorderWidth?: number; // bù trừ border của element cha (px)
maxHeight?: number; // chiều cao tối đa popup (px)
maxWidth?: number; // chiều rộng tối đa popup (px)
direction?: TYPE_POPOVER_DIRECTION; // hướng mở: 'bottom' | 'top' | 'left' | 'right'
ignoreArrow?: boolean; // ẩn mũi tên
classInclude?: string; // class CSS bổ sung cho container popup
disable?: boolean; // vô hiệu hóa popover
clickExactly?: boolean; // chỉ mở khi click chính xác
paddingLeftItem?: boolean; // thêm padding trái cho item
timerDestroy?: number; // thời gian trễ trước khi destroy (ms)
position?: {
mode: TYPE_POPOVER_POSITION_MODE; // 'start' | 'center' | 'end'
distance: number; // khoảng cách từ điểm neo (px)
};
animationConfig?: {
time?: number; // thời gian animation (ms)
distance?: number; // khoảng cách dịch chuyển animation (px)
};
width?: number; // chiều rộng cố định (px)
classIncludeOverlayBody?: string; // class CSS cho overlay body
}
// Cấu hình từng tab trong dropdown có tabs
interface IDropdownTabsItem {
key: string; // định danh duy nhất của tab
name: string; // tên hiển thị (hỗ trợ i18n key)
httpRequestData?: IHttpRequestConfig; // config API riêng cho tab này
}
// Validation giới hạn số item được chọn tối đa
interface IValidMaxItemSelected extends IMessageTranslate {
value: number; // số lượng tối đa cho phép chọn
}
// Cấu hình dropdown dùng để truyền vào service/helper
interface IDropdown {
listConfig: IListConfigItem;
listBackgroundListCustom?: string;
listMaxItemShow?: number;
classIncludePopup?: string;
paddingLeftItem?: boolean;
clickExactly?: boolean;
zIndex?: number;
popoverCustomConfig?: IPopoverCustomConfig;
disable?: boolean;
}
// Interface FunctionControl để điều khiển dropdown từ bên ngoài
interface IDropdownFunctionControlEvent {
checkIsValid: () => Promise<boolean>;
resetError: () => Promise<void>;
setError?: (message: string) => Promise<void>;
removeList: () => Promise<void>;
updateLabelItemSelected: (label: string) => Promise<void>;
reset: () => Promise<void>;
refreshList: () => Promise<void>;
setItemSelectedByKey: (id: unknown) => Promise<void>;
setSelectedKey: (key: unknown, options?: { reset?: boolean }) => Promise<void>;
setSelectedMultiKey: (keys: unknown[], options?: { reset?: boolean }) => Promise<void>;
getDisable: () => Promise<boolean>;
}Sub-Component: libs_ui-components-dropdown-tabs
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.
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [tabsConfig] | Array<IDropdownTabsItem> | undefined | Danh sách cấu hình tab | [tabsConfig]="tabs" |
| [(tabKeyActive)] | string (model) | undefined | Two-way binding key tab đang active | [(tabKeyActive)]="activeKey" |
| [ignoreBorderBottom] | boolean | undefined | Ẩn border bottom của tabs bar | [ignoreBorderBottom]="true" |
| [disable] | boolean | undefined | Vô hiệu hóa toàn bộ tabs | [disable]="true" |
| Output | Type | Mô tả |
|---|---|---|
| (outChange) | void | Emit mỗi khi chọn tab mới |
Lưu ý quan trọng
⚠️ 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.
⚠️ Chế độ chọn đơn vs nhiều: Dùng [(listKeySelected)] + (outSelectKey) cho type text/radio. Dùng [(listMultiKeySelected)] + (outSelectMultiKey) cho type checkbox/group.
⚠️ 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.
⚠️ 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".
⚠️ 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.
⚠️ 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ả.
⚠️ listKeysHidden không dùng kèm configCheckboxCheckAll: Tương tự listKeysDisable, tránh dùng kết hợp với configCheckboxCheckAll.
⚠️ 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.
