@libs-ui/components-buttons-dropdown
v0.2.357-5
Published
> Dropdown button với menu tùy chọn dạng popover, hỗ trợ hai chế độ: apply ngay khi chọn hoặc chọn trước rồi xác nhận sau.
Readme
@libs-ui/components-buttons-dropdown
Dropdown button với menu tùy chọn dạng popover, hỗ trợ hai chế độ: apply ngay khi chọn hoặc chọn trước rồi xác nhận sau.
Giới thiệu
LibsUiComponentsButtonsDropdownComponent là một standalone Angular component kết hợp button và popover menu thành một khối tương tác hoàn chỉnh. Component cung cấp hai chế độ hoạt động: applyNow = true để emit ngay khi user chọn item, hoặc applyNow = false để tách biệt nút mở menu và nút xác nhận (Apply). Nội dung item được sanitize tự động qua escapeHtml để ngăn XSS.
Tính năng
- ✅ Hai chế độ hoạt động: apply ngay (
applyNow = true) hoặc chọn rồi xác nhận (applyNow = false) - ✅ Two-way binding cho item đang được chọn qua
[(keySelected)] - ✅ Tùy chỉnh tên field hiển thị (
fieldDisplay) và field key (keyField) — không cần format lại data - ✅ Hỗ trợ icon bên trái cho từng item qua
fieldClassIconLeft - ✅ Custom sub-template cho item (
subTemplate) và button lồng trong item (buttonTemplateConfig) - ✅ Popover cấu hình linh hoạt: hướng mở, kích thước, z-index, animation
- ✅ Truy cập programmatic qua getter
FunctionsControlđể mở/đóng popup từ bên ngoài - ✅ XSS-safe: giá trị
fieldDisplaycủa mọi item tự động quaescapeHtml - ✅ Hỗ trợ disable toàn bộ button hoặc từng item riêng lẻ
- ✅ Angular Signals +
ChangeDetectionStrategy.OnPush— hiệu năng tối ưu
Khi nào sử dụng
- Cần button gọi menu dropdown chọn một giá trị (filter, sort, action)
- Cần tách luồng "chọn trước — xác nhận sau" (ví dụ: chọn kỳ báo cáo rồi bấm Apply)
- Cần action menu với icon và label cho từng mục
- Cần dropdown lồng popover với vị trí và kích thước tùy chỉnh theo context giao diện
Cài đặt
npm install @libs-ui/components-buttons-dropdownImport
import {
LibsUiComponentsButtonsDropdownComponent,
IButtonDropdown,
IPopupConfigButtonDropdown,
} from '@libs-ui/components-buttons-dropdown';
@Component({
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
// ...
})
export class YourComponent {}Ví dụ sử dụng
1. Apply ngay — chọn là emit
// component.ts
import { Component } from '@angular/core';
import { LibsUiComponentsButtonsDropdownComponent } from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-example',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './example.component.html',
})
export class ExampleComponent {
readonly statusOptions = [
{ key: 'active', label: 'Đang hoạt động' },
{ key: 'inactive', label: 'Ngừng hoạt động' },
{ key: 'pending', label: 'Chờ duyệt' },
];
handlerSelectStatus(event: Event, item: { key: string; label: string }): void {
event.stopPropagation();
console.log('Đã chọn:', item);
}
}<!-- example.component.html -->
<libs_ui-components-buttons-dropdown
label="Trạng thái"
[items]="statusOptions"
[applyNow]="true"
(outSelectItem)="handlerSelectStatus($event, $event)" />2. Chọn trước — xác nhận sau (applyNow = false)
// component.ts
import { Component, signal } from '@angular/core';
import { LibsUiComponentsButtonsDropdownComponent } from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-filter',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './filter.component.html',
})
export class FilterComponent {
protected selectedPeriodKey = signal<string>('q1');
readonly periodOptions = [
{ key: 'q1', label: 'Quý 1' },
{ key: 'q2', label: 'Quý 2' },
{ key: 'q3', label: 'Quý 3' },
{ key: 'q4', label: 'Quý 4' },
];
handlerApplyPeriod(event: Event, item: { key: string; label: string }): void {
event.stopPropagation();
console.log('Áp dụng kỳ:', item);
}
}<!-- filter.component.html -->
<libs_ui-components-buttons-dropdown
label="Chọn kỳ"
[items]="periodOptions"
[applyNow]="false"
[(keySelected)]="selectedPeriodKey"
(outApply)="handlerApplyPeriod($event, $event)" />3. Custom tên field — data không cần format lại
// component.ts
import { Component } from '@angular/core';
import { LibsUiComponentsButtonsDropdownComponent } from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-user-picker',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './user-picker.component.html',
})
export class UserPickerComponent {
readonly users = [
{ userId: 'u1', fullName: 'Nguyễn Văn An' },
{ userId: 'u2', fullName: 'Trần Thị Bình' },
{ userId: 'u3', fullName: 'Lê Quốc Cường' },
];
handlerSelectUser(event: Event, user: { userId: string; fullName: string }): void {
event.stopPropagation();
console.log('Người được chọn:', user.fullName);
}
}<!-- user-picker.component.html -->
<libs_ui-components-buttons-dropdown
label="Chọn người dùng"
[items]="users"
fieldDisplay="fullName"
keyField="userId"
[applyNow]="true"
(outSelectItem)="handlerSelectUser($event, $event)" />4. Items có icon
// component.ts
import { Component } from '@angular/core';
import { LibsUiComponentsButtonsDropdownComponent } from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-action-menu',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './action-menu.component.html',
})
export class ActionMenuComponent {
readonly actionItems = [
{ key: 'edit', label: 'Chỉnh sửa', classIconLeft: 'libs-ui-icon-edit-line' },
{ key: 'duplicate', label: 'Nhân bản', classIconLeft: 'libs-ui-icon-copy' },
{ key: 'delete', label: 'Xóa', classIconLeft: 'libs-ui-icon-remove' },
];
handlerSelectAction(event: Event, action: { key: string; label: string }): void {
event.stopPropagation();
console.log('Hành động:', action.key);
}
}<!-- action-menu.component.html -->
<libs_ui-components-buttons-dropdown
label="Thao tác"
[items]="actionItems"
fieldClassIconLeft="classIconLeft"
[applyNow]="true"
(outSelectItem)="handlerSelectAction($event, $event)" />5. Tùy chỉnh cấu hình popover
// component.ts
import { Component } from '@angular/core';
import {
LibsUiComponentsButtonsDropdownComponent,
IPopupConfigButtonDropdown,
} from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-custom-popup',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './custom-popup.component.html',
})
export class CustomPopupComponent {
readonly menuItems = [
{ key: '1', label: 'Mục 1' },
{ key: '2', label: 'Mục 2' },
{ key: '3', label: 'Mục 3' },
];
readonly popupConfig: IPopupConfigButtonDropdown = {
width: 280,
maxHeight: 200,
direction: 'bottom',
zIndex: 1500,
position: { mode: 'end', distance: 0 },
};
handlerSelectMenu(event: Event, item: { key: string }): void {
event.stopPropagation();
console.log('Chọn:', item.key);
}
}<!-- custom-popup.component.html -->
<libs_ui-components-buttons-dropdown
label="Tùy chọn"
[items]="menuItems"
[popupConfig]="popupConfig"
[applyNow]="true"
(outSelectItem)="handlerSelectMenu($event, $event)" />6. Các kiểu button (typeButton)
<div class="flex gap-2">
<libs_ui-components-buttons-dropdown
label="Primary"
[items]="menuItems"
typeButton="button-primary"
[applyNow]="true" />
<libs_ui-components-buttons-dropdown
label="Secondary"
[items]="menuItems"
typeButton="button-secondary"
[applyNow]="true" />
<libs_ui-components-buttons-dropdown
label="Outline"
[items]="menuItems"
typeButton="button-outline"
[applyNow]="true" />
</div>7. Truy cập programmatic qua FunctionsControl (ViewChild)
// parent.component.ts
import { Component, viewChild } from '@angular/core';
import { LibsUiComponentsButtonsDropdownComponent } from '@libs-ui/components-buttons-dropdown';
@Component({
selector: 'app-parent',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
templateUrl: './parent.component.html',
})
export class ParentComponent {
private readonly dropdownRef = viewChild<LibsUiComponentsButtonsDropdownComponent>('dropdownRef');
handlerOpenDropdown(event: Event): void {
event.stopPropagation();
this.dropdownRef()?.FunctionsControl?.show?.();
}
handlerCloseDropdown(event: Event): void {
event.stopPropagation();
this.dropdownRef()?.FunctionsControl?.hide?.();
}
}<!-- parent.component.html -->
<libs_ui-components-buttons-dropdown
#dropdownRef
label="Menu"
[items]="menuItems"
[applyNow]="true"
(outFunctionsControl)="handlerFunctionsControl($event)" />@Input()
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [applyNow] | boolean | false | true: emit ngay khi chọn item; false: tách nút mở menu và nút Apply | [applyNow]="true" |
| [classIconLeft] | string | '' | Class icon hiển thị bên trái label trên button chính | classIconLeft="libs-ui-icon-filter" |
| [classIconRight] | string | 'libs-ui-icon-move-right rotate-[90deg]' | Class icon mũi tên bên phải trên nút toggle menu (chế độ applyNow=false) | classIconRight="libs-ui-icon-chevron-right" |
| [classInclude] | string | '' | Class CSS bổ sung cho button | classInclude="w-full" |
| [classIncludeContainer] | string | undefined | Class CSS bổ sung cho div container bọc ngoài | classIncludeContainer="flex-1" |
| [classLabel] | string | '' | Class CSS cho label text trên button | classLabel="libs-ui-font-h5m" |
| [disable] | boolean | false | Disable toàn bộ button | [disable]="isLoading()" |
| [fieldClass] | string | 'class' | Tên field trong object item chứa class CSS của item đó | fieldClass="cssClass" |
| [fieldClassIconLeft] | string | 'classIconLeft' | Tên field trong object item chứa class icon bên trái | fieldClassIconLeft="iconClass" |
| [fieldDisplay] | string | 'label' | Tên field trong object item dùng để hiển thị text | fieldDisplay="name" |
| [iconOnlyType] | boolean | false | Chỉ hiển thị icon, ẩn label trên button | [iconOnlyType]="true" |
| [ignoreHiddenPopoverContentWhenMouseLeave] | boolean | true | Giữ popover mở khi chuột di ra ngoài vùng content | [ignoreHiddenPopoverContentWhenMouseLeave]="false" |
| [items] | Array<any> | required | Danh sách items hiển thị trong menu | [items]="options" |
| [keyField] | string | 'key' | Tên field trong object item dùng làm giá trị key định danh | keyField="id" |
| [(keySelected)] | string | undefined | Key của item đang được chọn — hỗ trợ two-way binding | [(keySelected)]="selectedKey" |
| [label] | string | undefined | Label mặc định của button (khi chưa chọn item nào, hoặc ở chế độ applyNow=true) | label="Chọn trạng thái" |
| [modePopover] | TYPE_POPOVER_MODE | 'click-toggle' | Chế độ kích hoạt popover | modePopover="click-toggle" |
| [popupConfig] | IPopupConfigButtonDropdown | { width: 205, maxWidth: 250, maxHeight: 140, zIndex: 1200, direction: 'top' } | Cấu hình chi tiết cho popover (kích thước, vị trí, z-index) | [popupConfig]="myConfig" |
| [showBorderBottom] | boolean | false | Hiển thị đường kẻ phân cách dưới mỗi item trong menu | [showBorderBottom]="true" |
| [sizeButton] | TYPE_SIZE_BUTTON | 'medium' | Kích thước button | sizeButton="small" |
| [typeButton] | TYPE_BUTTON | 'button-primary' | Kiểu button (màu sắc, style) | typeButton="button-secondary" |
@Output()
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outApply) | any | Emit item được chọn khi bấm nút Apply (chỉ ở chế độ applyNow = false) | handlerApply(event: Event, item: any): void { event.stopPropagation(); console.log(item); } | (outApply)="handlerApply($event, $event)" |
| (outFunctionsControl) | IPopoverFunctionControlEvent | Emit object chứa các hàm điều khiển popover (show, hide) ngay sau khi popover khởi tạo | handlerFunctionsControl(event: Event, ctrl: IPopoverFunctionControlEvent): void { event.stopPropagation(); } | (outFunctionsControl)="handlerFunctionsControl($event, $event)" |
| (outHover) | boolean | Emit false mỗi khi user vừa chọn xong một item | handlerHover(event: Event, state: boolean): void { event.stopPropagation(); } | (outHover)="handlerHover($event, $event)" |
| (outIconEvent) | MouseEvent | Emit MouseEvent khi click vào icon của button | handlerIconClick(event: MouseEvent): void { event.stopPropagation(); } | (outIconEvent)="handlerIconClick($event)" |
| (outPopoverEvent) | TYPE_POPOVER_EVENT | Emit các sự kiện lifecycle của popover (open, close, ...) | handlerPopoverEvent(event: Event, evt: TYPE_POPOVER_EVENT): void { event.stopPropagation(); } | (outPopoverEvent)="handlerPopoverEvent($event, $event)" |
| (outSelectItem) | any | Emit object item khi user click chọn một mục trong menu | handlerSelectItem(event: Event, item: any): void { event.stopPropagation(); console.log(item); } | (outSelectItem)="handlerSelectItem($event, $event)" |
Public API — FunctionsControl
Truy cập qua viewChild để điều khiển popover từ component cha:
import { Component, viewChild } from '@angular/core';
import {
LibsUiComponentsButtonsDropdownComponent,
IButtonDropdown,
} from '@libs-ui/components-buttons-dropdown';
import { IPopoverFunctionControlEvent } from '@libs-ui/components-popover';
@Component({
selector: 'app-demo',
standalone: true,
imports: [LibsUiComponentsButtonsDropdownComponent],
template: `
<libs_ui-components-buttons-dropdown
#myDropdown
label="Menu"
[items]="items"
[applyNow]="true"
(outFunctionsControl)="handlerControl($event)" />
`,
})
export class DemoComponent {
private readonly dropdownRef = viewChild<LibsUiComponentsButtonsDropdownComponent>('myDropdown');
protected items = [
{ key: '1', label: 'Mục 1' },
{ key: '2', label: 'Mục 2' },
];
handlerControl(ctrl: IPopoverFunctionControlEvent): void {
// ctrl được lưu bên trong component — gọi show/hide từ bên ngoài
}
// Mở popup từ bên ngoài
openMenu(): void {
this.dropdownRef()?.FunctionsControl?.show?.();
}
// Đóng popup từ bên ngoài
closeMenu(): void {
this.dropdownRef()?.FunctionsControl?.hide?.();
}
}| Getter | Return Type | Mô tả |
|---|---|---|
| FunctionsControl | IPopoverFunctionControlEvent \| undefined | Trả về object chứa các hàm điều khiển popover. undefined nếu popover chưa khởi tạo |
Cấu trúc Item
Mỗi phần tử trong mảng items hỗ trợ các thuộc tính sau:
interface DropdownItem {
// Bắt buộc (tên field tùy theo fieldDisplay và keyField)
label: string; // field hiển thị — mặc định key là 'label'
key: string; // field key định danh — mặc định key là 'key'
// Tùy chọn — styling
class?: string; // Class CSS cho span hiển thị label của item (ghi đè 'libs-ui-font-h5r')
classIconLeft?: string; // Class icon hiển thị bên trái label item
classInclude?: string; // Class CSS bổ sung cho div wrapper của item
classRow?: string; // Class CSS cho div row ngoài cùng của item
// Tùy chọn — behavior
disable?: boolean; // Disable item (pointer-events-none + visual disabled)
ignoreFlex?: boolean; // Bỏ display:flex cho wrapper item
showPopover?: boolean; // Hiển thị tooltip/popover riêng cho item này
popoverContent?: string; // Nội dung tooltip khi showPopover = true
// Tùy chọn — advanced
subTemplate?: TemplateRef<unknown>; // Custom Angular template render bên trong item
buttonTemplateConfig?: { // Render thêm một button bên trong item
icon?: string; // Class icon bên phải button
iconLeft?: string; // Class icon bên trái button
label?: string; // Label của button
type?: TYPE_BUTTON; // Kiểu button
action: (item: DropdownItem, allItems: DropdownItem[]) => void; // Callback khi click
};
}Types & Interfaces
import {
IButtonDropdown,
IPopupConfigButtonDropdown,
} from '@libs-ui/components-buttons-dropdown';// IButtonDropdown — kế thừa IButton, mô tả đầy đủ inputs của component
export interface IButtonDropdown extends IButton {
items: any[];
fieldDisplay?: string;
keyField?: string;
keySelected?: string;
applyNow?: boolean;
showBorderBottom?: boolean;
popupConfig?: IPopupConfigButtonDropdown;
ignoreHiddenPopoverContentWhenMouseLeave?: boolean;
modePopover?: TYPE_POPOVER_MODE;
}
// IPopupConfigButtonDropdown — cấu hình popover
export interface IPopupConfigButtonDropdown {
width?: number; // Chiều rộng cố định (px)
maxWidth?: number; // Chiều rộng tối đa (px)
maxHeight?: number; // Chiều cao tối đa của danh sách (px)
zIndex?: number; // z-index của popover layer
direction?: TYPE_POPOVER_DIRECTION; // Hướng mở: 'top' | 'bottom' | 'left' | 'right'
timeDestroy?: number; // Thời gian (ms) trước khi DOM popover bị destroy sau khi đóng
widthByParent?: boolean; // Lấy chiều rộng theo parent element
position?: {
mode: 'start' | 'center' | 'end'; // Căn lề ngang của popup so với trigger
distance: number; // Khoảng lệch theo chiều ngang (px)
};
classInclude?: string; // Class CSS bổ sung cho container popover
}Lưu ý quan trọng
⚠️ items là required: Input [items] bắt buộc phải truyền vào. Truyền mảng rỗng [] để hiển thị trạng thái "Không có dữ liệu".
⚠️ applyNow = false yêu cầu keyField: Khi dùng chế độ apply sau, component cần keyField để theo dõi item đang được chọn. Giá trị mặc định là 'key' — đảm bảo mỗi item có field key hoặc truyền keyField phù hợp.
⚠️ Label button thay đổi khi applyNow = false: Ở chế độ applyNow = false, sau khi user chọn item, label trên button chính sẽ tự động cập nhật sang tên item được chọn (dựa theo fieldDisplay). Để label cố định, dùng applyNow = true.
⚠️ XSS tự động: Giá trị của field fieldDisplay trong từng item được tự động xử lý qua escapeHtml khi truyền vào component. Không cần escape thủ công trước khi truyền.
⚠️ FunctionsControl có thể undefined: Getter FunctionsControl trả về undefined nếu popover chưa được mount. Luôn kiểm tra ?. trước khi gọi các method bên trong.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/buttons/dropdown
