@libs-ui/components-drag-drop
v0.2.357-0
Published
> Bộ Angular directives hỗ trợ kéo thả (drag & drop) linh hoạt: sắp xếp trong container, kéo thả qua nhiều container, auto-scroll, custom boundary và tích hợp virtual scroll.
Downloads
3,435
Readme
@libs-ui/components-drag-drop
Bộ Angular directives hỗ trợ kéo thả (drag & drop) linh hoạt: sắp xếp trong container, kéo thả qua nhiều container, auto-scroll, custom boundary và tích hợp virtual scroll.
Giới thiệu
@libs-ui/components-drag-drop cung cấp bộ directives dựa trên mouse events (không dùng HTML5 Drag API) để xử lý kéo thả chính xác hơn trên mọi trình duyệt. Lib hỗ trợ hai chế độ: move (di chuyển item) và copy (sao chép item), đồng thời tự động cập nhật danh sách qua two-way binding [(items)]. Thiết kế theo mô hình Container + Item: directive container quản lý danh sách, directive item gắn lên từng phần tử có thể kéo.
Tính năng
- Kéo thả để sắp xếp lại thứ tự item trong cùng một container
- Kéo thả item qua nhiều container (cross-container) với kiểm soát group
- Chế độ
copy: sao chép item sang container đích thay vì di chuyển - Placeholder trực quan tại vị trí drop trong khi kéo
- Auto-scroll khi kéo gần mép container (
LibsUiComponentsDragScrollDirective) - Hỗ trợ virtual scroll (
@iharbeck/ngx-virtual-scroller) cho danh sách lớn - Giới hạn vùng kéo trong boundary tùy chỉnh (
dragBoundary) - Animation hướng kéo:
horizontalhoặcvertical - Vô hiệu hóa kéo thả theo container hoặc từng item riêng lẻ
- Two-way binding
[(items)]— danh sách tự động cập nhật sau mỗi lần drop - Hỗ trợ ghi đè CSS styles qua
[stylesOverride]
Khi nào sử dụng
- Sắp xếp lại danh sách items bằng thao tác kéo thả
- Triển khai Kanban board với nhiều cột kéo thả qua lại
- Di chuyển hoặc sao chép items giữa hai danh sách (source/target)
- Danh sách lớn cần kết hợp virtual scroll để tối ưu hiệu năng
- Giới hạn vùng kéo thả trong một khu vực cụ thể (dashboard, canvas)
- Bất kỳ UI nào cần thao tác kéo thả mượt mà không phụ thuộc HTML5 Drag API
Cài đặt
npm install @libs-ui/components-drag-dropImport
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
LibsUiComponentsDragScrollDirective,
LibsUiDragItemInContainerVirtualScrollDirective,
} from '@libs-ui/components-drag-drop';
// Interfaces (dùng trong handler)
import {
IDragStart,
IDragOver,
IDragLeave,
IDragEnd,
IDrop,
IItemDragInfo,
IMousePosition,
IDragDropFunctionControlEvent,
} from '@libs-ui/components-drag-drop';Ví dụ sử dụng
Ví dụ 1 — Sắp xếp danh sách đơn giản
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
IDrop,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-basic-drag',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div
LibsUiComponentsDragContainerDirective
[(items)]="items"
[acceptDragSameGroup]="true"
(outDroppedContainer)="handlerDrop($event)"
class="min-h-[120px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-200">
@for (item of items(); track item.id) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item">
<div class="p-3 bg-white border border-gray-200 rounded-lg">
{{ item.name }}
</div>
</div>
}
</div>
`,
})
export class BasicDragComponent {
items = signal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
{ id: 4, name: 'Item 4' },
]);
handlerDrop(event: IDrop): void {
event.stopPropagation?.();
// items đã tự động cập nhật qua [(items)] two-way binding
console.log('Dropped:', event.itemDragInfo?.indexDrag, '->', event.itemDragInfo?.indexDrop);
}
}Ví dụ 2 — Kéo thả giữa hai container (Cross-Container)
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
IDrop,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-cross-container',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="flex gap-4">
<!-- Container nguồn: source có thể drop sang target -->
<div class="flex-1">
<h4 class="text-sm font-medium mb-2">Source</h4>
<div
LibsUiComponentsDragContainerDirective
[(items)]="sourceItems"
[groupName]="'source'"
[dropToGroupName]="['target']"
[acceptDragSameGroup]="true"
(outDroppedContainer)="handlerDrop($event)"
class="min-h-[120px] p-2 bg-blue-50 rounded-lg border-2 border-dashed border-blue-200">
@for (item of sourceItems(); track item) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective>
<div class="p-3 bg-white border border-blue-200 rounded-lg">{{ item }}</div>
</div>
}
</div>
</div>
<!-- Container đích: target có thể drop sang source -->
<div class="flex-1">
<h4 class="text-sm font-medium mb-2">Target</h4>
<div
LibsUiComponentsDragContainerDirective
[(items)]="targetItems"
[groupName]="'target'"
[dropToGroupName]="['source']"
[acceptDragSameGroup]="true"
(outDroppedContainer)="handlerDrop($event)"
class="min-h-[120px] p-2 bg-green-50 rounded-lg border-2 border-dashed border-green-200">
@for (item of targetItems(); track item) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective>
<div class="p-3 bg-white border border-green-200 rounded-lg">{{ item }}</div>
</div>
}
</div>
</div>
</div>
`,
})
export class CrossContainerComponent {
sourceItems = signal(['Source A', 'Source B', 'Source C']);
targetItems = signal(['Target X', 'Target Y']);
handlerDrop(event: IDrop): void {
event.stopPropagation?.();
console.log('Dropped item info:', event.itemDragInfo);
}
}Ví dụ 3 — Kanban Board
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
IDragStart,
IDrop,
} from '@libs-ui/components-drag-drop';
interface KanbanTask {
id: number;
title: string;
desc: string;
}
interface KanbanColumn {
id: string;
title: string;
items: KanbanTask[];
}
@Component({
selector: 'app-kanban-board',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="flex gap-4">
@for (column of columns(); track column.id) {
<div class="flex-1 bg-gray-50 rounded-lg p-3">
<h4 class="text-sm font-semibold text-gray-700 mb-3 pb-2 border-b border-gray-200">
{{ column.title }}
</h4>
<div
LibsUiComponentsDragContainerDirective
[(items)]="column.items"
[groupName]="column.id"
[dropToGroupName]="allColumnIds"
[acceptDragSameGroup]="true"
(outDragStartContainer)="handlerDragStart($event)"
(outDroppedContainer)="handlerDrop($event)"
class="min-h-[80px]">
@for (task of column.items; track task.id) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="task" [fieldId]="'id'">
<div class="p-3 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow">
<div class="font-medium text-gray-800 text-sm">{{ task.title }}</div>
<div class="text-xs text-gray-500 mt-1">{{ task.desc }}</div>
</div>
</div>
}
</div>
</div>
}
</div>
`,
})
export class KanbanBoardComponent {
columns = signal<KanbanColumn[]>([
{
id: 'todo',
title: 'To Do',
items: [
{ id: 1, title: 'Design UI', desc: 'Thiết kế giao diện người dùng' },
{ id: 2, title: 'Setup API', desc: 'Cấu hình REST API' },
],
},
{
id: 'progress',
title: 'In Progress',
items: [{ id: 3, title: 'Implement Auth', desc: 'Xây dựng xác thực JWT' }],
},
{
id: 'done',
title: 'Done',
items: [{ id: 4, title: 'Project Setup', desc: 'Khởi tạo project Angular' }],
},
]);
allColumnIds = ['todo', 'progress', 'done'];
handlerDragStart(event: IDragStart): void {
event.stopPropagation?.();
console.log('Drag started:', event.mousePosition);
}
handlerDrop(event: IDrop): void {
event.stopPropagation?.();
console.log('Task moved:', event.itemDragInfo?.item);
}
}Ví dụ 4 — Chế độ Copy (sao chép item)
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-copy-mode',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div class="flex gap-4">
<!-- Nguồn: mode copy — item không bị xóa khỏi nguồn -->
<div class="flex-1">
<h4 class="text-sm font-medium mb-2">Nguồn (copy mode)</h4>
<div
LibsUiComponentsDragContainerDirective
[(items)]="palette"
[groupName]="'palette'"
[dropToGroupName]="['canvas']"
[mode]="'copy'">
@for (item of palette(); track item.id) {
<div class="pb-2 cursor-copy" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
<div class="p-3 bg-blue-100 border border-blue-300 rounded-lg">{{ item.label }}</div>
</div>
}
</div>
</div>
<!-- Canvas: item được sao chép vào đây -->
<div class="flex-1">
<h4 class="text-sm font-medium mb-2">Canvas</h4>
<div
LibsUiComponentsDragContainerDirective
[(items)]="canvas"
[groupName]="'canvas'"
[acceptDragSameGroup]="true"
class="min-h-[200px] p-2 bg-gray-50 rounded-lg border-2 border-dashed border-gray-300">
@for (item of canvas(); track item.id) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
<div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.label }}</div>
</div>
}
</div>
</div>
</div>
`,
})
export class CopyModeComponent {
palette = signal([
{ id: 'btn-primary', label: 'Button Primary' },
{ id: 'btn-secondary', label: 'Button Secondary' },
{ id: 'input-text', label: 'Text Input' },
]);
canvas = signal<Array<{ id: string; label: string }>>([]);
}Ví dụ 5 — Auto-scroll với LibsUiComponentsDragScrollDirective
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
LibsUiComponentsDragScrollDirective,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-auto-scroll',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
LibsUiComponentsDragScrollDirective,
],
template: `
<!-- rootElementScroll trỏ đến element có overflow scroll -->
<div #scrollContainer class="h-[300px] overflow-y-auto border border-gray-200 rounded-lg">
<div
LibsUiComponentsDragContainerDirective
LibsUiComponentsDragScrollDirective
[(items)]="longList"
[acceptDragSameGroup]="true"
[rootElementScroll]="scrollContainer"
[widthZoneDetect]="24"
[movementLength]="8"
class="p-2">
@for (item of longList(); track item.id) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
<div class="p-3 bg-white border border-gray-200 rounded-lg">{{ item.name }}</div>
</div>
}
</div>
</div>
`,
})
export class AutoScrollComponent {
longList = signal(
Array.from({ length: 50 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` }))
);
}@Input() — LibsUiComponentsDragContainerDirective
Selector: [LibsUiComponentsDragContainerDirective]
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [(items)] | Array<unknown> | bắt buộc | Danh sách items (two-way binding). Tự động cập nhật sau mỗi lần drop. | [(items)]="myList" |
| [acceptDragSameGroup] | boolean | false | Cho phép kéo thả và sắp xếp lại trong cùng container/group. | [acceptDragSameGroup]="true" |
| [directionDrag] | 'horizontal' \| 'vertical' | undefined | Hướng kéo thả để thêm animation translate cho item bị đè qua. | [directionDrag]="'vertical'" |
| [disableDragContainer] | boolean | undefined | Vô hiệu hóa toàn bộ chức năng kéo thả trong container này. | [disableDragContainer]="isLocked" |
| [dropToGroupName] | Array<string> \| null | null | Danh sách groupName của các container khác mà item trong container này được phép drop vào. | [dropToGroupName]="['target', 'archive']" |
| [groupName] | string | 'groupDragAndDropDefault' | Tên định danh của container/group. Các container cùng tên có thể drop qua lại. | [groupName]="'todo'" |
| [mode] | 'move' \| 'copy' | 'move' | Chế độ kéo thả: move di chuyển item, copy sao chép item sang container đích. | [mode]="'copy'" |
| [placeholder] | boolean | true | Hiển thị placeholder (vị trí mờ) tại chỗ item sẽ được thả vào trong khi đang kéo. | [placeholder]="false" |
| [stylesOverride] | Array<{ className: string; styles: string }> | undefined | Ghi đè CSS styles mặc định của container và items. | [stylesOverride]="customStyles" |
| [viewEncapsulation] | 'emulated' \| 'none' | 'emulated' | Chế độ View Encapsulation cho styles inject. Dùng 'none' nếu cần styles xuyên shadow DOM. | [viewEncapsulation]="'none'" |
@Output() — LibsUiComponentsDragContainerDirective
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outDragStartContainer) | IDragStart | Emit khi bắt đầu kéo một item thuộc container này. | handlerDragStart(e: IDragStart): void { e.stopPropagation?.(); } | (outDragStartContainer)="handlerDragStart($event)" |
| (outDragOverContainer) | IDragOver | Emit khi item đang kéo di chuyển vào trong vùng container (enter). | handlerDragOver(e: IDragOver): void { e.stopPropagation?.(); } | (outDragOverContainer)="handlerDragOver($event)" |
| (outDragLeaveContainer) | IDragLeave | Emit khi item đang kéo rời khỏi vùng container (leave). | handlerDragLeave(e: IDragLeave): void { e.stopPropagation?.(); } | (outDragLeaveContainer)="handlerDragLeave($event)" |
| (outDragEndContainer) | IDragEnd | Emit khi kết thúc thao tác kéo (mouseup) trong container này. | handlerDragEnd(e: IDragEnd): void { e.stopPropagation?.(); } | (outDragEndContainer)="handlerDragEnd($event)" |
| (outDroppedContainer) | IDrop | Emit khi item được thả vào container này (bao gồm từ container khác). | handlerDrop(e: IDrop): void { e.stopPropagation?.(); } | (outDroppedContainer)="handlerDrop($event)" |
| (outDroppedContainerEmpty) | IDrop | Emit khi item được thả vào container khi container đang rỗng (không có item). | handlerDropEmpty(e: IDrop): void { e.stopPropagation?.(); } | (outDroppedContainerEmpty)="handlerDropEmpty($event)" |
| (outFunctionControl) | IDragDropFunctionControlEvent | Emit object chứa hàm điều khiển container từ bên ngoài khi ngAfterViewInit. | handlerFnControl(e: IDragDropFunctionControlEvent): void { this.containerFn = e; } | (outFunctionControl)="handlerFnControl($event)" |
@Input() — LibsUiDragItemDirective
Selector: [LibsUiDragItemDirective]
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [disable] | boolean | undefined | Vô hiệu hóa kéo thả cho riêng item này. | [disable]="item.locked" |
| [dragBoundary] | boolean | undefined | Giới hạn vùng kéo trong boundary của container (không cho kéo ra ngoài). | [dragBoundary]="true" |
| [dragBoundaryAcceptMouseLeaveContainer] | boolean | undefined | Khi bật dragBoundary, cho phép con trỏ chuột rời khỏi container mà item vẫn cập nhật vị trí theo boundary. | [dragBoundaryAcceptMouseLeaveContainer]="true" |
| [dragRootElement] | boolean | undefined | Kéo trực tiếp element gốc (không tạo clone). Element gốc sẽ ẩn đi trong khi kéo và hiện lại khi thả. | [dragRootElement]="true" |
| [elementContainer] | HTMLElement | undefined | Chỉ định element container tùy chỉnh để tính toán vị trí khi dragBoundary bật. | [elementContainer]="containerRef" |
| [fieldId] | string | '' | Tên field dùng làm ID định danh item. Bắt buộc khi dùng với virtual scroll. | [fieldId]="'id'" |
| [ignoreStopEvent] | boolean | undefined | Bỏ qua preventDefault() và stopPropagation() trên mouse events khi kéo. | [ignoreStopEvent]="true" |
| [ignoreUserSelectNone] | boolean | undefined | Không thêm class select-none vào item khi đang kéo. | [ignoreUserSelectNone]="true" |
| [item] | any | undefined | Dữ liệu của item. Bắt buộc khi dùng với virtual scroll hoặc khi cần IItemDragInfo trong event. | [item]="task" |
| [itemInContainerVirtualScroll] | boolean | undefined | Đánh dấu item nằm trong virtual scroll container. Bắt buộc bật khi dùng @iharbeck/ngx-virtual-scroller. | [itemInContainerVirtualScroll]="true" |
| [onlyMouseDownStopEvent] | boolean | undefined | Chỉ gọi stopPropagation() trên sự kiện mousedown, không chặn mousemove/mouseup. | [onlyMouseDownStopEvent]="true" |
| [throttleTimeHandlerDraggingEvent] | number | 0 | Thời gian throttle (ms) cho sự kiện dragging. Tăng giá trị để giảm tải khi list lớn. | [throttleTimeHandlerDraggingEvent]="16" |
| [zIndex] | number | 1300 | Giá trị z-index của phần tử clone khi đang kéo. | [zIndex]="2000" |
@Output() — LibsUiDragItemDirective
| Output | Type | Mô tả | Handler TS | Binding HTML |
|---|---|---|---|---|
| (outDragStart) | IDragStart | Emit khi bắt đầu kéo item này. | handlerItemDragStart(e: IDragStart): void { e.stopPropagation?.(); } | (outDragStart)="handlerItemDragStart($event)" |
| (outDragOver) | IDragOver | Emit khi một item khác đang được kéo qua item này. | handlerItemDragOver(e: IDragOver): void { e.stopPropagation?.(); } | (outDragOver)="handlerItemDragOver($event)" |
| (outDragLeave) | IDragLeave | Emit khi item đang kéo rời khỏi vùng của item này. | handlerItemDragLeave(e: IDragLeave): void { e.stopPropagation?.(); } | (outDragLeave)="handlerItemDragLeave($event)" |
| (outDragEnd) | IDragEnd | Emit khi kết thúc kéo item này (mouseup). | handlerItemDragEnd(e: IDragEnd): void { e.stopPropagation?.(); } | (outDragEnd)="handlerItemDragEnd($event)" |
| (outDropped) | IDrop | Emit khi item khác được thả lên item này. | handlerItemDropped(e: IDrop): void { e.stopPropagation?.(); } | (outDropped)="handlerItemDropped($event)" |
@Input() — LibsUiComponentsDragScrollDirective
Selector: [LibsUiComponentsDragScrollDirective]
Directive bổ sung, dùng chung với LibsUiComponentsDragContainerDirective để bật tính năng tự động cuộn container khi kéo item đến gần mép.
| Input | Type | Default | Mô tả | Ví dụ |
|---|---|---|---|---|
| [ignoreAutoScroll] | boolean | undefined | Tắt hoàn toàn tính năng auto-scroll. | [ignoreAutoScroll]="true" |
| [movementLength] | number | 6 | Số pixel scroll mỗi tick khi item đang gần mép container. | [movementLength]="10" |
| [rootElementScroll] | HTMLElement | undefined | Trỏ đến element có overflow: scroll/auto để thực hiện scroll. Nếu không có, scroll trên host element. | [rootElementScroll]="scrollEl" |
| [virtualScrollerComponent] | VirtualScrollerComponent | undefined | Tham chiếu đến VirtualScrollerComponent khi dùng @iharbeck/ngx-virtual-scroller. | [virtualScrollerComponent]="vsRef" |
| [widthZoneDetect] | number | 16 | Chiều rộng (px) của vùng phát hiện gần mép để kích hoạt auto-scroll. | [widthZoneDetect]="24" |
Types & Interfaces
import {
IDragging,
IDragStart,
IDragOver,
IDragLeave,
IDragEnd,
IDrop,
IItemDragInfo,
IMousePosition,
IDragDropFunctionControlEvent,
IDragItemInContainerVirtualScroll,
} from '@libs-ui/components-drag-drop';
/** Event khi đang kéo (mousemove) */
interface IDragging {
mousePosition: IMousePosition;
elementDrag: HTMLElement; // Clone element đang được kéo
elementKeepContainer?: boolean; // true nếu chuột nằm ngoài mọi container
itemDragInfo?: IItemDragInfo;
}
/** Event khi bắt đầu kéo */
interface IDragStart {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
/** Event khi item đang kéo di chuyển qua element khác */
interface IDragOver {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
elementDragOver: HTMLElement; // Element đang bị hover qua
itemDragInfo?: IItemDragInfo;
}
/** Event khi item đang kéo rời khỏi element */
interface IDragLeave {
elementDrag: HTMLElement;
elementDragLeave: HTMLElement; // Element vừa bị rời khỏi
itemDragInfo?: IItemDragInfo;
}
/** Event khi kết thúc kéo (mouseup) */
interface IDragEnd {
mousePosition: IMousePosition;
elementDrag: HTMLElement;
itemDragInfo?: IItemDragInfo;
}
/** Event khi drop item vào container/item khác */
interface IDrop {
elementDrag: HTMLElement;
elementDrop: HTMLElement; // Container hoặc item đích nhận drop
itemDragInfo?: IItemDragInfo;
}
/** Thông tin chi tiết về item đang/vừa được kéo */
interface IItemDragInfo {
item: object; // Dữ liệu của item
itemsMove?: WritableSignal<Array<unknown>>; // Danh sách nguồn sau khi đã xóa item kéo (dùng cho move mode)
indexDrag?: number; // Vị trí (index) ban đầu của item trong container nguồn
indexDrop?: number; // Vị trí (index) cuối cùng của item sau khi drop
itemsDrag: WritableSignal<Array<unknown>>; // Signal danh sách của container nguồn
itemsDrop?: WritableSignal<Array<unknown>>; // Signal danh sách của container đích
containerDrag?: HTMLElement; // HTMLElement của container nguồn
containerDrop?: HTMLElement; // HTMLElement của container đích
}
/** Vị trí con trỏ chuột */
interface IMousePosition {
clientX: number;
clientY: number;
}
/** Object functions điều khiển container emit qua (outFunctionControl) */
interface IDragDropFunctionControlEvent {
/** Đồng bộ lại attributes và index cho tất cả items trong container */
setAttributeElementAndItemDrag: () => Promise<void>;
}CSS Classes mặc định
| Class | Mô tả |
|---|---|
| .libs-ui-drag-drop-container | Tự động được thêm vào element có LibsUiComponentsDragContainerDirective |
| .libs-ui-drag-drop-container-dragover | Được thêm khi có item đang kéo vào trong container |
| .libs-ui-drag-drop-item | Tự động được thêm vào element có LibsUiDragItemDirective |
| .libs-ui-drag-drop-item-dragging | Được thêm vào clone element đang được kéo (cursor: move) |
| .libs-ui-drag-drop-item-placeholder | Được thêm vào item gốc khi nó đang bị kéo (dùng để ẩn bằng CSS) |
| .libs-ui-drag-drop-item-origin-placeholder | Item gốc đang bị kéo (trong container nguồn) |
| .libs-ui-drag-drop-item-drop-placeholder | Item tạm thời (ghost) hiển thị tại vị trí drop đích |
| .libs-ui-drag-drop-item-translate-top | Animation: item dịch chuyển lên khi kéo theo chiều vertical |
| .libs-ui-drag-drop-item-translate-bottom | Animation: item dịch chuyển xuống khi kéo theo chiều vertical |
| .libs-ui-drag-drop-item-translate-left | Animation: item dịch chuyển sang trái khi kéo theo chiều horizontal |
| .libs-ui-drag-drop-item-translate-right | Animation: item dịch chuyển sang phải khi kéo theo chiều horizontal |
Tùy chỉnh giao diện qua stylesOverride
import { Component, signal } from '@angular/core';
import {
LibsUiComponentsDragContainerDirective,
LibsUiDragItemDirective,
} from '@libs-ui/components-drag-drop';
@Component({
selector: 'app-custom-styled',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [LibsUiComponentsDragContainerDirective, LibsUiDragItemDirective],
template: `
<div
LibsUiComponentsDragContainerDirective
[(items)]="items"
[acceptDragSameGroup]="true"
[stylesOverride]="customStyles">
@for (item of items(); track item.id) {
<div class="pb-2 cursor-move" LibsUiDragItemDirective [item]="item" [fieldId]="'id'">
<div class="p-3 bg-white border rounded-lg">{{ item.name }}</div>
</div>
}
</div>
`,
})
export class CustomStyledDragComponent {
items = signal([
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
]);
customStyles = [
{
className: 'libs-ui-drag-drop-item-origin-placeholder',
styles: `
.libs-ui-drag-drop-item-origin-placeholder {
opacity: 0.3;
background: #e0f2fe;
}
`,
},
{
className: 'libs-ui-drag-drop-item-drop-placeholder',
styles: `
.libs-ui-drag-drop-item-drop-placeholder {
border: 2px dashed #3b82f6;
background: #eff6ff;
}
`,
},
];
}Lưu ý quan trọng
⚠️ Two-way binding bắt buộc: [(items)] là bắt buộc trên LibsUiComponentsDragContainerDirective. Danh sách items tự động cập nhật sau mỗi lần drop — không cần cập nhật thủ công trong handler.
⚠️ Dùng padding thay vì margin giữa các item: Khoảng cách giữa các drag item PHẢI dùng padding (ví dụ: class pb-2), TUYỆT ĐỐI KHÔNG dùng margin (ví dụ: mb-2). Lý do: khi dùng margin, vùng margin giữa hai 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 item xuống cuối thay vì vào đúng vị trí.
⚠️ Group Name cho cross-container: Khi kéo thả giữa các container, phải thiết lập [groupName] để định danh container và [dropToGroupName] để chỉ định danh sách container đích được phép nhận drop. Hai container muốn kéo qua lại cần cài [dropToGroupName] chỉ vào nhau.
⚠️ Virtual scroll: Khi dùng với @iharbeck/ngx-virtual-scroller, bắt buộc thêm [itemInContainerVirtualScroll]="true" trên LibsUiDragItemDirective, cung cấp [fieldId] (tên field ID), [item] (dữ liệu item), và thêm LibsUiDragItemInContainerVirtualScrollDirective lên component cha.
⚠️ Mode copy: Với [mode]="'copy'", item được sao chép (deep clone) sang container đích, không bị xóa khỏi container nguồn. Đảm bảo các item trong nguồn có field ID duy nhất khi dùng [fieldId] để tránh xung đột.
⚠️ ViewEncapsulation: Mặc định là 'emulated'. Đổi sang 'none' nếu component dùng ViewEncapsulation.None hoặc cần styles của placeholder/placeholder xuyên qua shadow DOM.
⚠️ outFunctionControl: Nếu cần gọi setAttributeElementAndItemDrag() từ bên ngoài (ví dụ: sau khi thêm item thủ công vào list), lắng nghe (outFunctionControl) để lưu reference object hàm điều khiển.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/drag-drop
