@libs-ui/components-drag-drop
v0.2.355-15
Published
> Thư viện Angular directives hỗ trợ kéo thả (drag and drop) linh hoạt, hỗ trợ kéo thả giữa các container, virtual scrolling, auto-scroll và custom boundary.
Readme
@libs-ui/components-drag-drop
Thư viện Angular directives hỗ trợ kéo thả (drag and drop) linh hoạt, hỗ trợ kéo thả giữa các container, virtual scrolling, auto-scroll và custom boundary.
Version
0.2.355-14
Author
Mobio Dev Team
Khi nào sử dụng
- Khi cần sắp xếp lại thứ tự các phần tử trong một danh sách bằng thao tác kéo thả.
- Khi cần di chuyển phần tử giữa nhiều container (cross-container drag and drop).
- Khi cần triển khai Kanban board với nhiều cột có thể kéo thả qua lại.
- Khi danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng.
- Khi cần giới hạn vùng kéo thả trong một boundary cụ thể.
- Khi cần auto-scroll khi kéo phần tử gần mép container.
Lưu ý quan trọng
⚠️ Two-way binding: [(items)] là bắt buộc trên Container directive. Danh sách items sẽ tự động cập nhật khi kéo thả.
⚠️ Group Name: Khi kéo thả giữa các container, cần thiết lập [groupName] và [dropToGroupName] để xác định nguồn và đích.
⚠️ Virtual Scroll: Khi sử dụng virtual scroll, cần bật [itemInContainerVirtualScroll]="true" trên Item directive và cung cấp [fieldId] để định danh item.
⚠️ ViewEncapsulation: Mặc định là 'emulated'. Chuyển sang 'none' nếu cần style xuyên qua shadow DOM.
⚠️ Mode copy: Khi [mode]="'copy'", item sẽ được sao chép thay vì di chuyển sang container đích.
⚠️ Performance: Sử dụng [throttleTimeHandlerDraggingEvent] để throttle event dragging, giảm tải khi danh sách lớn.
⚠️ Padding thay vì Margin: Khoảng cách giữa các drag item PHẢI dùng padding (ví dụ pb-2), KHÔNG dùng margin (ví dụ mb-2). Khi dùng margin, vùng margin giữa các item không thuộc bất kỳ element nào, khiến thư viện không phát hiện được vị trí hover chính xác và sẽ thêm phần tử xuống cuối danh sách thay vì vị trí đang hover.
Cài đặt & Import
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
LibsUiComponentsDragScrollDirective,
LibsUiDragItemInContainerVirtualScrollDirective,
} from '@libs-ui/components-drag-drop';Ví dụ sử dụng
Basic - Sắp xếp danh sách đơn giản
import { Component } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-basic-drag-drop',
standalone: true,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div
LibsUiComponentsDragContainerDirective
[(items)]="items">
<div
*ngFor="let item of items"
LibsUiDragItemDirective
[item]="item">
{{ item.name }}
</div>
</div>
`,
})
export class BasicDragDropComponent {
items = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
];
}Cross Container - Kéo thả giữa hai container
@Component({
standalone: true,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="flex gap-4">
<!-- Container nguồn -->
<div
LibsUiComponentsDragContainerDirective
[(items)]="sourceItems"
[groupName]="'source'"
[dropToGroupName]="['target']"
(outDroppedContainer)="onDrop($event)">
<div
*ngFor="let item of sourceItems"
LibsUiDragItemDirective
[item]="item">
{{ item.name }}
</div>
</div>
<!-- Container đích -->
<div
LibsUiComponentsDragContainerDirective
[(items)]="targetItems"
[groupName]="'target'"
[dropToGroupName]="['source']"
(outDroppedContainer)="onDrop($event)">
<div
*ngFor="let item of targetItems"
LibsUiDragItemDirective
[item]="item">
{{ item.name }}
</div>
</div>
</div>
`,
})
export class CrossContainerComponent {
sourceItems = [
{ id: 1, name: 'Source 1' },
{ id: 2, name: 'Source 2' },
];
targetItems = [
{ id: 3, name: 'Target 1' },
{ id: 4, name: 'Target 2' },
];
onDrop(event: IDrop) {
console.log('Dropped:', event);
}
}Kanban Board
@Component({
standalone: true,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="flex gap-4">
@for (column of columns; track column.id) {
<div class="flex-1 bg-gray-100 p-4 rounded-lg">
<h3>{{ column.title }}</h3>
<div
LibsUiComponentsDragContainerDirective
[(items)]="column.items"
[groupName]="column.id"
[dropToGroupName]="allGroupNames"
[directionDrag]="'vertical'">
@for (item of column.items; track item.id) {
<div
class="bg-white p-3 mb-2 rounded shadow-sm"
LibsUiDragItemDirective
[item]="item">
{{ item.title }}
</div>
}
</div>
</div>
}
</div>
`,
})
export class KanbanBoardComponent {
columns = [
{ id: 'todo', title: 'To Do', items: [{ id: 1, title: 'Task 1' }] },
{ id: 'doing', title: 'Doing', items: [{ id: 2, title: 'Task 2' }] },
{ id: 'done', title: 'Done', items: [{ id: 3, title: 'Task 3' }] },
];
allGroupNames = ['todo', 'doing', 'done'];
}Virtual Scroll
@Component({
standalone: true,
imports: [
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
LibsUiComponentsDragScrollDirective,
LibsUiDragItemInContainerVirtualScrollDirective,
],
template: `
<virtual-scroller #scroll [items]="items">
<div
LibsUiComponentsDragContainerDirective
LibsUiComponentsDragScrollDirective
[(items)]="items">
<div
*ngFor="let item of scroll.viewPortItems"
LibsUiDragItemDirective
[item]="item"
[itemInContainerVirtualScroll]="true"
[fieldId]="'id'">
{{ item.name }}
</div>
</div>
</virtual-scroller>
`,
})
export class VirtualScrollDragDropComponent {
items = Array.from({ length: 1000 }, (_, i) => ({ id: i, name: `Item ${i}` }));
}Custom Boundary - Giới hạn vùng kéo thả
@Component({
standalone: true,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="relative w-[500px] h-[500px] border-2 border-gray-800">
<div
LibsUiComponentsDragContainerDirective
[(items)]="items">
<div
*ngFor="let item of items"
LibsUiDragItemDirective
[item]="item"
[dragBoundary]="true">
{{ item.name }}
</div>
</div>
</div>
`,
})
export class CustomBoundaryComponent {
items = [
{ id: 1, name: 'Bounded Item 1' },
{ id: 2, name: 'Bounded Item 2' },
];
}API Reference
LibsUiComponentsDragContainerDirective
Selector: [LibsUiComponentsDragContainerDirective]
Inputs
| Property | Type | Default | Mô tả |
|---|---|---|---|
| [acceptDragSameGroup] | boolean | false | Cho phép kéo thả trong cùng group. |
| [directionDrag] | 'horizontal' \| 'vertical' | undefined | Hướng kéo thả (ngang hoặc dọc). |
| [disableDragContainer] | boolean | undefined | Vô hiệu hóa toàn bộ kéo thả trong container. |
| [dropToGroupName] | Array<string> \| null | null | Danh sách group name được phép drop vào container này. |
| [groupName] | string | 'groupDragAndDropDefault' | Tên group để định danh container. |
| [(items)] | Array<unknown> | bắt buộc | Danh sách items (two-way binding). |
| [mode] | 'move' \| 'copy' | 'move' | Chế độ kéo thả: di chuyển hoặc sao chép. |
| [placeholder] | boolean | true | Hiển thị placeholder tại vị trí drop. |
| [stylesOverride] | Array<{className, styles}> | undefined | Ghi đè styles cho container. |
| [viewEncapsulation] | 'emulated' \| 'none' | 'emulated' | Chế độ View Encapsulation cho styles. |
Outputs
| Property | Type | Mô tả |
|---|---|---|
| (outDragEndContainer) | IDragEnd | Emit khi kết thúc kéo phần tử trong container. |
| (outDragLeaveContainer) | IDragLeave | Emit khi phần tử kéo rời khỏi container. |
| (outDragOverContainer) | IDragOver | Emit khi phần tử kéo di chuyển qua container. |
| (outDragStartContainer) | IDragStart | Emit khi bắt đầu kéo phần tử trong container. |
| (outDroppedContainer) | IDrop | Emit khi thả phần tử vào container. |
| (outDroppedContainerEmpty) | IDrop | Emit khi thả phần tử vào container rỗng. |
| (outFunctionControl) | IDragDropFunctionControlEvent | Emit object chứa các hàm điều khiển container. |
LibsUiDragItemDirective
Selector: [LibsUiDragItemDirective]
Inputs
| Property | Type | Default | Mô tả |
|---|---|---|---|
| [disable] | boolean | undefined | Vô hiệu hóa kéo thả cho item này. |
| [dragBoundary] | boolean | undefined | Giới hạn kéo trong vùng boundary của container. |
| [dragBoundaryAcceptMouseLeaveContainer] | boolean | undefined | Cho phép chuột rời container khi đang giới hạn boundary. |
| [dragRootElement] | boolean | undefined | Sử dụng root element làm gốc kéo. |
| [elementContainer] | HTMLElement | undefined | Element container tùy chỉnh cho item. |
| [fieldId] | string | '' | Tên field dùng làm ID định danh item. |
| [ignoreStopEvent] | boolean | undefined | Bỏ qua stop event mặc định. |
| [ignoreUserSelectNone] | boolean | undefined | Bỏ qua việc set user-select: none khi kéo. |
| [item] | any | undefined | Dữ liệu của item. |
| [itemInContainerVirtualScroll] | boolean | undefined | Đánh dấu item nằm trong virtual scroll container. |
| [onlyMouseDownStopEvent] | boolean | undefined | Chỉ stop event khi mousedown. |
| [throttleTimeHandlerDraggingEvent] | number | 0 | Thời gian throttle (ms) cho dragging event. |
| [zIndex] | number | 1300 | z-index của phần tử khi đang kéo. |
Outputs
| Property | Type | Mô tả |
|---|---|---|
| (outDragEnd) | IDragEnd | Emit khi kết thúc kéo item. |
| (outDragLeave) | IDragLeave | Emit khi item rời khỏi vùng drop. |
| (outDragOver) | IDragOver | Emit khi item di chuyển qua vùng drop. |
| (outDragStart) | IDragStart | Emit khi bắt đầu kéo item. |
| (outDropped) | IDrop | Emit khi item được thả. |
LibsUiComponentsDragScrollDirective
Selector: [LibsUiComponentsDragScrollDirective]
Directive tự động scroll container khi phần tử kéo di chuyển gần mép.
Inputs
| Property | Type | Default | Mô tả |
|---|---|---|---|
| [ignoreAutoScroll] | boolean | undefined | Tắt tính năng auto scroll. |
| [widthZoneDetect] | number | 16 | Chiều rộng vùng phát hiện (px) gần mép để kích hoạt scroll. |
| [movementLength] | number | 6 | Khoảng cách scroll mỗi lần (px). |
| [rootElementScroll] | HTMLElement | undefined | Element gốc để scroll (mặc định là host element). |
| [virtualScrollerComponent] | VirtualScrollerComponent | undefined | Tham chiếu đến VirtualScrollerComponent. |
LibsUiDragItemInContainerVirtualScrollDirective
Selector: [LibsUiDragItemInContainerVirtualScrollDirective]
Directive hỗ trợ kéo thả item trong container sử dụng virtual scroll (@iharbeck/ngx-virtual-scroller).
Types & Interfaces
export interface IDragging {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
elementKeepContainer?: boolean;
itemDragInfo?: IItemDragInfo;
}
export interface IDragStart {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
export interface IDragOver {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
elementDragOver: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
export interface IDragLeave {
elementDrag: HTMLElement;
elementDragLeave: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
export interface IDragEnd {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
export interface IDrop {
elementDrag: HTMLElement;
elementDrop: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
export interface IItemDragInfo {
item: object;
itemsMove?: WritableSignal<Array<unknown>>;
indexDrag?: number;
indexDrop?: number;
itemsDrag: WritableSignal<Array<unknown>>;
itemsDrop?: WritableSignal<Array<unknown>>;
containerDrag?: HTMLElement;
containerDrop?: HTMLElement;
}
export interface IDragItemInContainerVirtualScroll {
itemDragInfo?: IItemDragInfo;
elementDrag: HTMLElement;
distanceStartElementAndMouseTop: number;
distanceStartElementAndMouseLeft: number;
elementContainer?: HTMLElement;
dragBoundary?: boolean;
dragBoundaryAcceptMouseLeaveContainer?: boolean;
ignoreStopEvent?: boolean;
ignoreUserSelectNone: boolean;
}
export interface IMousePosition {
clientX: number;
clientY: number;
}
export interface IDragDropFunctionControlEvent {
setAttributeElementAndItemDrag: () => Promise<void>;
}Tùy chỉnh giao diện (Styling)
CSS Classes mặc định
| Class | Mô tả |
|---|---|
| .libs-ui-drag-drop-container | Class cho container kéo thả. |
| .libs-ui-drag-drop-item | Class cho mỗi item kéo thả. |
| .libs-ui-drag-drop-item-dragging | Class được thêm vào item đang được kéo. |
| .libs-ui-drag-drop-item-placeholder | Class cho placeholder tại vị trí drop. |
Sử dụng stylesOverride
Ghi đè styles thông qua input [stylesOverride] trên Container directive:
@Component({
standalone: true,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div
LibsUiComponentsDragContainerDirective
[(items)]="items"
[stylesOverride]="customStyles">
<div
*ngFor="let item of items"
LibsUiDragItemDirective
[item]="item">
{{ item.name }}
</div>
</div>
`,
})
export class CustomStyledComponent {
items = [{ id: 1, name: 'Item 1' }];
customStyles = [
{
className: 'custom-drag-container',
styles: `
.custom-drag-container {
background: #f5f5f5;
border-radius: 8px;
padding: 16px;
}
`,
},
{
className: 'custom-drag-item-dragging',
styles: `
.custom-drag-item-dragging {
opacity: 0.6;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
`,
},
];
}Demo
- Local: http://localhost:4500/drag-drop
