@libs-ui/services-diagram-draw
v0.2.357-2
Published
> Bộ ba service Angular quản lý toàn bộ vòng đời diagram flow: tính toán tọa độ, render node động, vẽ SVG connector, quản lý kéo-thả và điều hướng luồng giữa các element.
Readme
@libs-ui/services-diagram-draw
Bộ ba service Angular quản lý toàn bộ vòng đời diagram flow: tính toán tọa độ, render node động, vẽ SVG connector, quản lý kéo-thả và điều hướng luồng giữa các element.
Giới thiệu
@libs-ui/services-diagram-draw cung cấp ba service độc lập phối hợp với nhau để xây dựng diagram flow tương tác:
LibsUiDiagramDrawService— service trung tâm: tính tọa độ X/Y, render component node vào canvas, vẽ đường SVG, quản lý drop zone kéo-thả, hỗ trợ virtualization cho diagram lớn.LibsUiDiagramDrawCanvasService— service quản lý cấu trúc dữ liệu diagram: thêm/xóa/thay thế element, xây dựng flat list, kiểm tra rule kết nối.LibsUiDiagramDrawDirectionService— service quản lý điều hướng (connection line) giữa các element không liền kề: thêm/xóa/chỉnh sửa đường nối tự do.
Hỗ trợ hai chế độ layout: vertical (dọc từ trên xuống, mặc định) và horizontal (ngang từ trái sang phải).
Tính năng
- Tính toán tọa độ tự động cho diagram dọc và ngang
- Render Angular component động vào canvas (main node và nodeOtherConfig)
- Vẽ SVG connector với cubic bezier tự động
- Hiển thị mũi tên chỉ hướng (vertical: ▼, horizontal: ►) giữa các node
- Render label trên nhánh (branch label) với màu sắc và class tùy chỉnh
- Drop zone kéo-thả với indicator "+" tùy biến hoàn toàn
- Virtualization — chỉ render node trong viewport, hỗ trợ diagram hàng trăm node
- Quản lý thêm/xóa/thay thế/di chuyển element trong cấu trúc linked-list
- Quản lý điều hướng tự do (next_other_id) và cleanup tự động khi xóa element
- Build dữ liệu navigation line để kết hợp với
@libs-ui/components-draw-line - Tự động tăng counter tên node khi thêm cùng loại (Email 1, Email 2, …)
- Kiểm tra rule kết nối (elementCodeConnectableBefore) trước khi cho phép drop
Khi nào sử dụng
- Xây dựng giao diện thiết kế luồng tự động hóa (automation flow, journey builder)
- Cần render động nhiều loại node component khác nhau trên cùng canvas
- Diagram có cấu trúc phân nhánh (branching workflow) phức tạp
- Cần tính năng kéo-thả từ sidebar vào luồng với validation rule
- Diagram cần kết nối điều hướng tự do giữa các element không liền kề
- Diagram lớn (>50 node) cần virtualization để tránh lag render
Cài đặt
npm install @libs-ui/services-diagram-drawImport
import {
LibsUiDiagramDrawService,
LibsUiDiagramDrawCanvasService,
LibsUiDiagramDrawDirectionService,
canvasConfigReadonly,
storeDataDefault,
} from '@libs-ui/services-diagram-draw';
import type {
IDiagramElement,
IDiagramElementBranch,
ICanvasConfig,
ICustomCanvasConfig,
IDiagramStoreData,
IDropZoneIndicatorStyle,
IRuleConnectable,
IHandlerFunction,
TDropZoneCanDropFn,
INavigationLineStyleConfig,
} from '@libs-ui/services-diagram-draw';Ba service đều có providedIn: 'root' — inject trực tiếp bằng inject(), không cần khai báo trong providers.
Ví dụ sử dụng
Ví dụ 1 — Khởi tạo diagram cơ bản (vertical)
import { Component, AfterViewInit, ElementRef, ViewContainerRef, inject, viewChild } from '@angular/core';
import {
LibsUiDiagramDrawService,
LibsUiDiagramDrawCanvasService,
storeDataDefault,
IDiagramElement,
IRuleConnectable,
} from '@libs-ui/services-diagram-draw';
import { MyNodeComponent } from './my-node.component';
@Component({
selector: 'app-diagram',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="position: relative; overflow: auto; width: 100%; height: 100vh;">
<svg #svgContainer style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<path style="fill: none; stroke: #9ca2ad; stroke-width: 1;"></path>
</svg>
<div #nodesContainer style="position: relative; width: 100%; height: 100%;"></div>
<ng-container #vcr></ng-container>
</div>
`,
})
export class DiagramComponent implements AfterViewInit {
private readonly svgContainer = viewChild<ElementRef<HTMLDivElement>>('svgContainer');
private readonly nodesContainer = viewChild<ElementRef<HTMLDivElement>>('nodesContainer');
private readonly vcr = viewChild('vcr', { read: ViewContainerRef });
private readonly diagramService = inject(LibsUiDiagramDrawService);
private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
private readonly elements: IDiagramElement[] = [
{ id: '1', code: 'EMAIL', name: 'Gửi Email 1', specific_width: 165, specific_height: 48, next_id: '2', pre_id: '' },
{ id: '2', code: 'SMS', name: 'Gửi SMS 1', specific_width: 165, specific_height: 48, next_id: '3', pre_id: '1' },
{ id: '3', code: 'EXIT', element_type: 'EXIT', name: 'Kết thúc', specific_width: 165, specific_height: 48, pre_id: '2' },
];
private readonly rules: IRuleConnectable[] = [
{ elementCode: 'SMS', elementCodeConnectableBefore: ['EMAIL', 'SMS'] },
{ elementCode: 'EMAIL', elementCodeConnectableBefore: ['EMAIL', 'SMS'] },
];
ngAfterViewInit() {
this.diagramService.setConfig = {
storeDataDefine: { ...storeDataDefault },
svgContainer: this.svgContainer(),
nodesContainer: this.nodesContainer(),
viewContainerRef: this.vcr(),
nodeComponentType: MyNodeComponent,
};
this.canvasService.loadElements(this.elements, { x: 200, y: 40 }, this.rules);
this.diagramService.configXYElements(this.elements);
this.diagramService.renderNodes(this.elements);
}
}Ví dụ 2 — Diagram ngang (horizontal) với drop zone
import { Component, AfterViewInit, ElementRef, ViewContainerRef, inject, viewChild } from '@angular/core';
import {
LibsUiDiagramDrawService,
LibsUiDiagramDrawCanvasService,
storeDataDefault,
IDiagramElement,
IDiagramElementBranch,
IRuleConnectable,
TDropZoneCanDropFn,
} from '@libs-ui/services-diagram-draw';
import { MyNodeComponent } from './my-node.component';
@Component({
selector: 'app-diagram-horizontal',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div style="position: relative; overflow: auto; width: 100%; height: 100vh;">
<svg #svgContainer style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none;">
<path style="fill: none; stroke: #9ca2ad; stroke-width: 1;"></path>
</svg>
<div #nodesContainer style="position: relative; width: 100%; height: 100%;"></div>
<ng-container #vcr></ng-container>
</div>
`,
})
export class DiagramHorizontalComponent implements AfterViewInit {
private readonly svgContainer = viewChild<ElementRef<HTMLDivElement>>('svgContainer');
private readonly nodesContainer = viewChild<ElementRef<HTMLDivElement>>('nodesContainer');
private readonly vcr = viewChild('vcr', { read: ViewContainerRef });
private readonly diagramService = inject(LibsUiDiagramDrawService);
private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
private readonly elements: IDiagramElement[] = [
{ id: '1', code: 'TRIGGER', name: 'Trigger 1', specific_width: 165, specific_height: 48, next_id: '2', pre_id: '' },
{ id: '2', code: 'ACTION', name: 'Action 1', specific_width: 165, specific_height: 48, next_id: '3', pre_id: '1' },
{ id: '3', code: 'EXIT', element_type: 'EXIT', name: 'Kết thúc', specific_width: 165, specific_height: 48, pre_id: '2' },
];
private readonly rules: IRuleConnectable[] = [
{ elementCode: 'ACTION', elementCodeConnectableBefore: ['TRIGGER', 'ACTION'] },
];
ngAfterViewInit() {
// Chuyển sang chế độ ngang
this.diagramService.orientation.set('horizontal');
this.diagramService.setConfig = {
storeDataDefine: { ...storeDataDefault },
svgContainer: this.svgContainer(),
nodesContainer: this.nodesContainer(),
viewContainerRef: this.vcr(),
nodeComponentType: MyNodeComponent,
};
this.canvasService.loadElements(this.elements, { x: 40, y: 200 }, this.rules);
this.diagramService.configXYElements(this.elements);
this.diagramService.renderNodes(this.elements);
this.renderDropZones();
}
private renderDropZones() {
const canDropFn: TDropZoneCanDropFn = (_before, current, _after, _rules) => {
// Trả về true = ẨN drop zone, false = HIỂN THỊ
return current?.code === 'EXIT';
};
this.diagramService.renderDropZones(
this.canvasService.FlatElements,
(element: IDiagramElement, branch: IDiagramElementBranch | undefined, dragData: string) => {
const data: IDiagramElement = JSON.parse(dragData);
this.canvasService.addElement(element, branch, data);
this.diagramService.configXYElements(this.elements);
this.diagramService.renderNodes(this.elements);
this.renderDropZones();
},
canDropFn,
this.rules,
);
}
}Ví dụ 3 — Diagram phân nhánh (WORKFLOW element)
const elementsWithBranch: IDiagramElement[] = [
{
id: '1',
code: 'START',
name: 'Bắt đầu',
specific_width: 165,
specific_height: 48,
next_id: '2',
pre_id: '',
},
{
id: '2',
code: 'CONDITION',
element_type: 'WORKFLOW',
name: 'Điều kiện 1',
specific_width: 165,
specific_height: 48,
next_id: '5',
pre_id: '1',
branches: [
{
code: 'YES',
label: 'Đúng',
labelBgColor: '#dcfce7',
labelColor: '#15803d',
elements: [
{ id: '3', code: 'EMAIL', name: 'Email 1', specific_width: 165, specific_height: 48, next_id: '', pre_id: '2' },
],
},
{
code: 'NO',
label: 'Sai',
labelBgColor: '#fee2e2',
labelColor: '#dc2626',
elements: [
{ id: '4', code: 'SMS', name: 'SMS 1', specific_width: 165, specific_height: 48, next_id: '', pre_id: '2' },
],
},
],
},
{
id: '5',
code: 'EXIT',
element_type: 'EXIT',
name: 'Kết thúc',
specific_width: 165,
specific_height: 48,
pre_id: '2',
},
];Ví dụ 4 — Virtualization cho diagram lớn
ngAfterViewInit() {
this.diagramService.setConfig = {
storeDataDefine: { ...storeDataDefault },
svgContainer: this.svgContainer(),
nodesContainer: this.nodesContainer(),
viewContainerRef: this.vcr(),
nodeComponentType: MyNodeComponent,
useVirtualizationTemplate: true, // Bật virtualization
};
this.diagramService.configXYElements(this.elements);
this.diagramService.renderNodes(this.elements);
// Cập nhật viewport khi scroll
const container = this.nodesContainer()?.nativeElement.parentElement;
container?.addEventListener('scroll', () => {
this.diagramService.updateViewport({
x: container.scrollLeft,
y: container.scrollTop,
width: container.clientWidth,
height: container.clientHeight,
});
});
}Ví dụ 5 — Navigation line (kết nối tự do)
import {
LibsUiDiagramDrawDirectionService,
LibsUiDiagramDrawCanvasService,
LibsUiDiagramDrawService,
INavigationLineStyleConfig,
} from '@libs-ui/services-diagram-draw';
// Trong component:
private readonly directionService = inject(LibsUiDiagramDrawDirectionService);
private readonly canvasService = inject(LibsUiDiagramDrawCanvasService);
private readonly diagramService = inject(LibsUiDiagramDrawService);
// Thêm điều hướng từ element tới target
addDirection(sourceElement: IDiagramElement, targetId: string) {
const flatElements = this.canvasService.FlatElements;
this.directionService.addElementDirection(targetId, sourceElement, flatElements);
}
// Build dữ liệu navigation line để render qua draw-line directive
buildNavigationLines() {
const styleConfig: INavigationLineStyleConfig = {
lineStyle: { color: '#6366f1', width: 1.5 },
arrowStyle: { color: '#6366f1', size: 8 },
endpointGap: 8,
};
const navData = this.diagramService.buildNavigationData(
this.canvasService.FlatElements,
styleConfig,
);
// Truyền navData vào drawLineFunctionControl.setData(navData)
}Methods — LibsUiDiagramDrawService
| Method | Signature | Mô tả |
|---|---|---|
| set setConfig | (config: ICustomCanvasConfig): void | Đăng ký config (container refs, component types). Phải gọi trước mọi method khác. |
| set setConfigCanvas | (config: Partial<ICanvasConfig>): void | Ghi đè tham số canvas config (margin, kích thước, màu SVG). |
| configXYElements | (elements: IDiagramElement[], updateSVG?: boolean, positionX?: number): void | Tính toán tọa độ X/Y cho tất cả elements và cập nhật SVG path. |
| renderNodes | (elements: IDiagramElement[]): void | Xóa node cũ, render component động, branch labels và arrow indicators. |
| clearNodes | (): void | Destroy tất cả ComponentRef, xóa .diagram-node-dynamic, .branch-label, .node-arrow-indicator khỏi DOM. |
| renderDropZones | (flatElements, onDrop, canDropFn, rules): void | Render drop zone "+" tại các vị trí cho phép kéo-thả. |
| clearDropZones | (): void | Xóa tất cả .diagram-drop-zone khỏi nodesContainer. |
| updateViewport | (vp: { x, y, width, height }): void | Cập nhật viewport để virtualization tính toán node hiển thị. Chỉ có hiệu lực khi useVirtualizationTemplate: true. |
| buildNavigationData | (flatElements: IDiagramElement[], styleConfig?: INavigationLineStyleConfig): IDrawLineDataInput[] | Build dữ liệu navigation line từ next_other_id để render qua draw-line directive. |
Chi tiết renderDropZones
renderDropZones(
flatElements: IDiagramElement[],
onDrop: (element: IDiagramElement, branch: IDiagramElementBranch | undefined, dragData: string) => void,
canDropFn: TDropZoneCanDropFn,
ruleConnectableElements: IRuleConnectable[],
): voidLưu ý: canDropFn trả về true → ẨN drop zone; false → HIỂN THỊ drop zone (ngược với convention thông thường).
Methods — LibsUiDiagramDrawCanvasService
| Method | Signature | Mô tả |
|---|---|---|
| loadElements | (elements: IDiagramElement[], positionFirstNode: { x, y }, rules: IRuleConnectable[]): void | Khởi tạo danh sách elements, đặt vị trí node đầu tiên, lưu rule kết nối. |
| addElement | (itemDrop, branchDrop, dataDrop, branchChoice?): void | Thêm element vào sau itemDrop, tự động tạo EXIT node cho WORKFLOW. |
| removeOrChangeElement | (elementRemove, elementChange, branchChoose?): Promise<void> | Xóa hoặc thay thế element, tự động thêm EXIT khi cần. |
| deleteChangeElementAndInsertBranchKeep | (elementRemove, elementChange, branchKeepElement, branchChoice?): Promise<void> | Xóa element và giữ nguyên nhánh chỉ định. |
| addElementExitForAllElementPreviousWhenElementDelete | (elementsDelete: IDiagramElement[]): void | Khi xóa hàng loạt element, tự động thêm EXIT cho các element đang trỏ vào vùng bị xóa. |
| checkRuleDragDrop | (before, current, after, rules): boolean | Kiểm tra rule kết nối trước khi cho phép drop. |
| checkExistNameElement | (name: string, flatElements: IDiagramElement[]): boolean | Kiểm tra tên element đã tồn tại chưa (case-insensitive). |
| get FlatElements | IDiagramElement[] | Trả về flat list của tất cả element (bao gồm element trong branches). |
| set Elements | (elements: IDiagramElement[]): void | Gán lại root elements array. |
| get Elements | IDiagramElement[] | Lấy root elements array. |
| set HandlerFunction | (fn: IHandlerFunction): void | Đăng ký callback tạo default config branches cho WORKFLOW element. |
Methods — LibsUiDiagramDrawDirectionService
| Method | Signature | Mô tả |
|---|---|---|
| buildElementConnectTo | (element, flatElements, branch?, checkRule?): IDiagramElement[] | Build danh sách element có thể kết nối tới. |
| addElementDirection | (targetId: string, element: IDiagramElement, flatElements: IDiagramElement[]): void | Thêm điều hướng từ element tới target (cập nhật next_other_id và pre_other_id). |
| addBranchDirection | (targetId, element, branch, flatElements): void | Thêm điều hướng từ branch của WORKFLOW element tới target. |
| editDirection | (flatElements, targetId, element, branch?): void | Thay đổi điều hướng hiện tại sang target mới. |
| deleteElementsBehindElementConnectTo | (element, flatElements): void | Xóa tất cả element phía sau khi element chuyển sang điều hướng. |
| deleteElementsBehindBranchConnectTo | (branch: IDiagramElementBranch): void | Xóa tất cả element trong branch khi branch chuyển sang điều hướng. |
| set ElementDirection | (element: IDiagramElement | undefined): void | Lưu element đang chọn điều hướng. |
| get ElementDirection | IDiagramElement | undefined | Lấy element đang chọn điều hướng. |
| set BranchDirection | (branch: { branch, index } | undefined): void | Lưu branch đang chọn điều hướng. |
| get OnViewDirection | Subject<{ id: string }> | Observable phát khi cần highlight element đang được xem qua điều hướng. |
Exported Constants
| Export | Type | Mô tả |
|---|---|---|
| canvasConfigReadonly | Signal<ICanvasConfig> (readonly) | Signal đọc canvas config global — dùng trong component mà không cần inject service. |
| storeDataDefault | IDiagramStoreData | Giá trị mặc định cho storeDataDefine trong ICustomCanvasConfig. |
Canvas Config (ICanvasConfig)
Tùy chỉnh qua set setConfigCanvas:
| Property | Default | Mô tả |
|---|---|---|
| ELEMENT_WIDTH_DEFAULT_BRANCH_WHEN_NOT_ELEMENT | 60 | Chiều rộng branch rỗng (không có element) |
| ELEMENT_MARGIN_DEFAULT | 60 | Khoảng cách dọc giữa các element (vertical) hoặc ngang (horizontal) |
| ELEMENT_MARGIN_DEFAULT_BRANCH | 16 | Khoảng cách từ element có nhánh đến đường rẽ nhánh |
| ELEMENT_MARGIN_BETWEEN_BRANCH_DEFAULT | 60 | Khoảng cách ngang giữa các branch (tối thiểu 32 để tương thích navigation line) |
| DEFAULT_BRANCH_WHEN_NO_ELEMENT | 100 | Chiều cao/rộng branch khi không có element |
| ELEMENT_HEIGHT_CURVE | 10 | Độ cong của SVG connector |
| ELEMENT_SVG_STROKE_COLOR | '9ca2ad' | Màu đường kẻ SVG (không có ký tự # đứng trước) |
| ELEMENT_SVG_STROKE_WIDTH | '1' | Độ dày đường kẻ SVG |
| ELEMENT_MARGIN_DEFAULT_BRANCH_TO_ELEMENT_FIRST | 78 | Khoảng cách từ element cha đến element đầu tiên trong branch |
| DISTANCE_TO_WAIT | 30 | Khoảng cách từ element chính đến nodeOtherConfig |
| WAIT_TO_ELEMENT | 50 | Khoảng cách từ nodeOtherConfig đến element kế tiếp |
| ELEMENT_WAIT_DEFAULT | 28 | Chiều cao mặc định của nodeOtherConfig |
| DISTANCE_WAIT_TO_NEXT_LINE | 4 | Khoảng cách từ nodeOtherConfig đến đường nối tiếp theo |
| TYPE_ELEMENT_EXIT | 'EXIT' | Giá trị code/element_type cho element kết thúc (không có drop zone bên dưới) |
| TYPE_ELEMENT_WORKFLOW | 'WORKFLOW' | Giá trị element_type cho element có branches |
| WIDTH_ELEMENT_DEFAULT | 165 | Chiều rộng mặc định node (dùng khi specific_width không được set) |
| HEIGHT_ELEMENT_DEFAULT | 48 | Chiều cao mặc định node (dùng khi specific_height không được set) |
| ADD_ICON_DIAMETER | 24 | Đường kính của icon "+" trong drop zone |
Ví dụ tùy chỉnh:
this.diagramService.setConfigCanvas = {
ELEMENT_MARGIN_DEFAULT: 80,
ELEMENT_SVG_STROKE_COLOR: '6366f1',
WIDTH_ELEMENT_DEFAULT: 200,
HEIGHT_ELEMENT_DEFAULT: 56,
};Types & Interfaces
import type {
IDiagramElement,
IDiagramElementBranch,
ICanvasConfig,
ICustomCanvasConfig,
IDiagramStoreData,
IDropZoneIndicatorStyle,
IRuleConnectable,
IHandlerFunction,
TDropZoneCanDropFn,
INavigationLineStyleConfig,
} from '@libs-ui/services-diagram-draw';IDiagramElement
interface IDiagramElement {
id?: string; // Unique identifier
code?: string; // Loại element (VD: 'EMAIL', 'SMS', 'EXIT', 'CONDITION')
name?: string; // Tên hiển thị
element_type?: string; // 'WORKFLOW' cho element có nhánh, 'EXIT' cho kết thúc
branches?: IDiagramElementBranch[]; // Danh sách nhánh (chỉ có khi element_type = 'WORKFLOW')
next_id?: string; // ID element kế tiếp trong luồng chính
pre_id?: string; // ID element trước trong luồng chính
next_other_id?: string[]; // ID các element được điều hướng đến (kết nối tự do)
pre_other_id?: string[]; // ID các element điều hướng đến element này
nodeOtherConfig?: IDiagramElement; // Node phụ (VD: wait node, delay config)
specific_x?: number; // Vị trí X do người dùng/service set trước khi tính toán
specific_y?: number; // Vị trí Y do người dùng/service set trước khi tính toán
specific_width?: number; // Chiều rộng node (ghi đè WIDTH_ELEMENT_DEFAULT)
specific_height?: number; // Chiều cao node (ghi đè HEIGHT_ELEMENT_DEFAULT)
translateX?: number; // Tọa độ X cuối cùng sau khi tính toán (computed)
translateY?: number; // Tọa độ Y cuối cùng sau khi tính toán (computed)
attributeSvgD?: string; // SVG path string cho connector
subCodeOfElement?: string; // Sub-type code dùng khi element có điều hướng
position_end?: number; // Vị trí cuối của đường nối (computed)
disable?: boolean; // Ẩn/vô hiệu hóa element
specific_counter_current_code?: number; // Counter đếm số lượng element cùng code
}IDiagramElementBranch
interface IDiagramElementBranch {
code?: string; // Định danh nhánh (VD: 'YES', 'NO')
label?: string; // Nhãn hiển thị trên nhánh (VD: 'Đúng', 'Sai')
labelBgColor?: string; // Màu nền nhãn (VD: '#dcfce7')
labelColor?: string; // Màu chữ nhãn (VD: '#15803d')
labelClassName?: string; // CSS class cho nhãn (mặc định: 'libs-ui-font-h7r')
elements: IDiagramElement[]; // Danh sách element trong nhánh
onTheSide?: 'above' | 'under' | 'center'; // Vị trí nhãn tương đối với nhánh
specific_start_branch_x?: number; // Tọa độ X điểm đầu đường nhánh (computed)
specific_start_branch_y?: number; // Tọa độ Y điểm đầu đường nhánh (computed)
next_other_id?: string[]; // ID các element được điều hướng từ nhánh này
isDirection?: boolean; // Nhánh đang ở chế độ điều hướng
nodeOtherConfig?: IDiagramElement; // Node phụ tại điểm rẽ nhánh
}ICustomCanvasConfig
interface ICustomCanvasConfig {
storeDataDefine: IDiagramStoreData; // Lưu trữ tọa độ max — dùng storeDataDefault
svgContainer?: ElementRef<HTMLDivElement>; // Ref đến SVG container để vẽ đường nối
nodesContainer?: ElementRef<HTMLDivElement>; // Ref đến div chứa node component
viewContainerRef?: ViewContainerRef; // ViewContainerRef để tạo component động
nodeComponentType?: Type<any>; // Component type cho main node
nodeOtherConfigComponentType?: Type<any>; // Component type cho nodeOtherConfig
useVirtualizationTemplate?: boolean; // Bật virtualization (mặc định: false)
dropZoneStyle?: IDropZoneIndicatorStyle; // Tùy biến giao diện indicator "+"
}IRuleConnectable
interface IRuleConnectable {
elementCode: string; // Code của element cần kiểm tra
elementCodeConnectableBefore: string[]; // Danh sách code được phép đứng trước
}IHandlerFunction
interface IHandlerFunction {
getConfigDefaultBranches(code: string, dataDefault: IDiagramElement): IDiagramElement;
}TDropZoneCanDropFn
type TDropZoneCanDropFn = (
beforeElementDrag: IDiagramElement | undefined, // Element trước vị trí drop
currentElementDrag: IDiagramElement | undefined, // Element tại vị trí drop
afterElementDrag: IDiagramElement | undefined, // Element sau vị trí drop
ruleConnectableElements: IRuleConnectable[],
) => boolean; // true = ẨN drop zone, false = HIỂN THỊINavigationLineStyleConfig
interface INavigationLineStyleConfig {
lineStyle?: ILineStyle; // Style đường kẻ (color, width, dash)
arrowStyle?: IArrowStyle; // Style mũi tên (color, size)
endpointGap?: number; // Khoảng cách từ cạnh node đến endpoint (mặc định: 8)
modeResolver?: (start: { x, y }, end: { x, y }) => TYPE_MODE; // Override chọn kiểu đường
}Tùy biến indicator "+" (IDropZoneIndicatorStyle)
Truyền vào dropZoneStyle của ICustomCanvasConfig để kiểm soát giao diện nút "+" trong drop zone.
Trạng thái bình thường
| Thuộc tính | Type | Mặc định | Mô tả |
|---|---|---|---|
| backgroundColor | string | '#3b82f6' | Màu nền của nút "+" |
| iconColor | string | '#ffffff' | Màu dấu "+" bên trong |
| borderRadius | string | '50%' | Bo góc — '50%' tròn, '6px' vuông |
| opacity | number | 0.95 | Độ mờ ở trạng thái bình thường |
| renderIndicator | (size: number) => string | — | Hàm trả về HTML tùy ý, ghi đè toàn bộ indicator |
Trạng thái hover/drag-enter
| Thuộc tính | Type | Mặc định | Mô tả |
|---|---|---|---|
| hoverBackgroundColor | string | '#eef4ff' | Màu nền vùng highlight khi kéo vào |
| hoverBorderColor | string | 'rgba(59, 130, 246, 0.7)' | Màu viền vùng highlight |
| hoverBorderStyle | string | 'dashed' | Kiểu viền: 'dashed', 'solid', 'dotted' |
| hoverBorderRadius | number | 10 | Border-radius (px) của vùng highlight |
| renderHoverIndicator | () => string | — | Hàm trả về HTML inject vào bên trong vùng highlight |
Ví dụ tùy biến màu sắc:
this.diagramService.setConfig = {
storeDataDefine: { ...storeDataDefault },
svgContainer: this.svgContainer(),
nodesContainer: this.nodesContainer(),
viewContainerRef: this.vcr(),
nodeComponentType: MyNodeComponent,
dropZoneStyle: {
backgroundColor: '#10b981',
iconColor: '#ffffff',
borderRadius: '4px',
opacity: 1,
hoverBackgroundColor: '#f0fdf4',
hoverBorderColor: 'rgba(16, 185, 129, 0.8)',
hoverBorderStyle: 'dashed',
hoverBorderRadius: 8,
},
};Ví dụ render indicator hoàn toàn tùy chỉnh:
this.diagramService.setConfig = {
storeDataDefine: { ...storeDataDefault },
svgContainer: this.svgContainer(),
nodesContainer: this.nodesContainer(),
viewContainerRef: this.vcr(),
nodeComponentType: MyNodeComponent,
dropZoneStyle: {
renderIndicator: (size: number) => `
<div style="
width: ${size}px; height: ${size}px;
background: #f59e0b; border-radius: 6px;
display: flex; align-items: center; justify-content: center;
">
<svg viewBox="0 0 24 24" fill="none" width="14" height="14">
<path d="M12 5v14M5 12h14" stroke="white" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
`,
renderHoverIndicator: () => `
<span style="color: #f59e0b; font-size: 11px; font-weight: 600;">
+ Thêm vào đây
</span>
`,
hoverBackgroundColor: '#fffbeb',
hoverBorderColor: 'rgba(245, 158, 11, 0.6)',
hoverBorderStyle: 'dashed',
hoverBorderRadius: 12,
},
};Lưu ý quan trọng
⚠️ Thứ tự khởi tạo bắt buộc: Phải gọi set setConfig trước mọi method khác của LibsUiDiagramDrawService.
⚠️ canvasConfig là signal global: Thay đổi qua set setConfigCanvas sẽ ảnh hưởng đến mọi instance trong cùng app. Cần cẩn thận khi có nhiều diagram trên cùng trang.
⚠️ renderNodes() tự gọi clearNodes() trước: Không cần gọi thủ công trước khi render lại.
⚠️ canDropFn logic ngược: Trả về true → ẨN drop zone; false → HIỂN THỊ drop zone. Đây là thiết kế có chủ ý để hàm đóng vai trò "filter loại trừ".
⚠️ ELEMENT_SVG_STROKE_COLOR không có ký tự #: Giá trị phải là mã hex thuần (VD: '9ca2ad', không phải '#9ca2ad').
⚠️ Virtualization yêu cầu scroll listener: Khi bật useVirtualizationTemplate: true, cần tự lắng nghe sự kiện scroll của container và gọi updateViewport() để service cập nhật node hiển thị.
⚠️ ELEMENT_MARGIN_BETWEEN_BRANCH_DEFAULT tối thiểu 32: Giá trị dưới 32px chưa tương thích với navigation line rendering.
⚠️ loadElements phải gọi trước configXYElements: LibsUiDiagramDrawCanvasService.loadElements() khởi tạo vị trí node đầu tiên, cần được gọi trước khi service tính toán tọa độ.
Demo
npx nx serve core-uiTruy cập: http://localhost:4500/services/diagram-draw
