jsPlumb Usage Guide
Recently, a project required implementing visual DAG (Directed Acyclic Graph) drawing. After researching several solutions, I ultimately chose jsPlumb.
jsPlumb
itself is divided into the paid Toolkit
version and the Community
community edition. Since the core functionality is already available in the community edition, and the open-source community’s activity level makes future development and maintenance more convenient, I chose the community edition.
Learning Resources
When learning a technology, official documentation, official communities, and Google searches are what you should rely on most.
Here are the addresses (repetitive, but important):
Besides these official resources, reading some blog posts can also be quite helpful. As I’m writing here, I hope it can help you somewhat.
Usage
My actual project uses Angular as the frontend framework, so the code here is written in that context. However, this shouldn’t significantly affect others from learning from it, as the thinking and concepts are similar - after all, it’s all JavaScript.
First, Understand jsPlumb Drawing Concepts
Endpoints Endpoints refer to the visual points on our diagram that can have connections going out or coming in.
Anchors
Anchors refer to the positions where endpoints can ultimately land. This means an endpoint can specify multiple anchor positions, flexibly landing on a particular anchor based on the actual graphic position.
Connections We establish relationships between multiple Windows (nodes) through connections. For example, whether we use Bezier curves or flowchart-style polyline connections requires setting up connectors.
Overlays Solve UI issues above drawing and connections, such as labels or arrows.
Emphasis: Actually, it's easy to misunderstand the concepts of anchors and endpoints - I went down some wrong paths myself.
Examples
Implemented Features
- Dynamic node addition
- Dynamic node deletion
- Dynamic edge connection
- Dynamic edge deletion
- Node drag listening
- Node and edge right-click menus
implemented with third-party component jquery.contextMenu
Show me the code
import {Component, ElementRef, OnInit, Renderer2, ViewChild} from '@angular/core';
declare let jsPlumb: any;
declare let $: any;
@Component({
selector: 'app-jsplumb',
templateUrl: './jsplumb.component.html',
styleUrls: ['./jsplumb.component.css']
})
export class JsplumbComponent implements OnInit {
jsPlumbInstance: any;
@ViewChild('canvas') public panel: ElementRef; // Canvas panel
constructor(private renderer: Renderer2) {
}
ngOnInit() {
this.draw();
}
draw() {
this.jsPlumbInstance = jsPlumb.getInstance({
// default drag options
DragOptions: {cursor: 'pointer', zIndex: 2000},
// the overlays to decorate each connection with. note that the label overlay uses a function to generate the label text; in this
// case it returns the 'labelText' member that we set on each connection in the 'init' method below.
ConnectionOverlays: [
['Arrow', {
location: 1,
visible: true,
width: 11,
length: 11,
id: 'ARROW',
events: {
click: function () {
alert('you clicked on the arrow overlay')
}
}
}],
['Label', {
location: 0.1,
id: 'label',
cssClass: 'aLabel',
events: {
// connection.getOverlay("label")
tap: function () {
let label = prompt('Please enter label text:');
this.setLabel(label);
}
}
}]
],
Container: 'canvas',
ConnectionsDetachable: true
});
const basicType = {
connector: ['Bezier', {curviness: 100}],
paintStyle: {stroke: 'red', strokeWidth: 4},
hoverPaintStyle: {stroke: 'blue'},
overlays: [
'Arrow'
]
};
this.jsPlumbInstance.registerConnectionType('basic', basicType);
// Enable dragging
this.jsPlumbInstance.draggable('flowchartWindow1');
this.jsPlumbInstance.draggable('flowchartWindow2');
this.jsPlumbInstance.draggable('flowchartWindow3');
this.jsPlumbInstance.draggable('flowchartWindow4');
// Add endpoints
this.jsPlumbInstance.addEndpoint('flowchartWindow1', sourceEndpoint);
this.jsPlumbInstance.addEndpoint('flowchartWindow2', targetEndpoint);
// listen for clicks on connections, and offer to delete connections on click.
//
this.jsPlumbInstance.bind('click', function (conn, originalEvent) {
// if (confirm("Delete connection from " + conn.sourceId + " to " + conn.targetId + "?"))
// instance.detach(conn);
// conn.toggleType('basic');
console.log(conn);
console.log(originalEvent);
});
//
this.jsPlumbInstance.bind('connection', (connInfo) => {
this.addMenu4Edge(connInfo);
console.log(connInfo);
});
this.jsPlumbInstance.bind('connectionDetached', (connInfo) => {
console.log(connInfo);
});
this.jsPlumbInstance.bind('connectionDrag', function (connection) {
console.log('connection ' + connection.id + ' is being dragged. suspendedElement is ', connection.suspendedElement, ' of type ', connection.suspendedElementType);
});
this.jsPlumbInstance.bind('connectionDragStop', function (connection) {
console.log('connection ' + connection.id + ' was dragged');
});
this.jsPlumbInstance.bind('connectionMoved', function (params) {
console.log('connection ' + params.connection.id + ' was moved');
});
this.jsPlumbInstance.bind('click', (connection, e) => {
this.jsPlumbInstance.detach(connection);
});
}
/**
* Add right-click menu for edges
*/
addMenu4Edge(connInfo) {
connInfo.connection.addClass(connInfo.connection.id);
const removeEdge = (v) => {
console.log(connInfo);
let cons = this.jsPlumbInstance.getConnections('*', {source: connInfo['sourceId'], target: connInfo['targetId']});
this.jsPlumbInstance.deleteConnection(cons[0]);
};
$.contextMenu({
selector: '.' + connInfo.connection.id,
callback: function (key, opt, event) {
console.log(`event`);
console.log(event);
},
items: {
'cut': {
name: 'Delete',
icon: 'cut',
callback: function (key, opt) {
removeEdge(key);
}
}
}
});
}
/**
* Add right-click menu for nodes
* id=nodeProgram-5
*/
addMenu4Node(nodeId: string) {
let removeNode = (v) => {
this.jsPlumbInstance.remove(nodeId);
};
$.contextMenu({
selector: '#' + nodeId,
callback: function (key, opt, event) {
console.log(`event`);
console.log(event);
},
items: {
'cut': {
name: 'Delete',
icon: 'cut',
callback: function (key, opt) {
removeNode(key);
}
}
}
});
}
addNode() {
// Add node information to chart
const div = this.renderer.createElement('div');
div.id = 'flowchartWindow5';
div.innerHTML = `<strong>End 5</strong><br/><br/>`;
div.setAttribute('class', 'window jtk-node');
this.renderer.appendChild(this.panel.nativeElement, div);
this.jsPlumbInstance.addEndpoint('flowchartWindow5', sourceEndpoint);
// Enable dragging
this.jsPlumbInstance.draggable($(div), {
drag: function (event) {
console.log(event);
},
start: function (event) {
console.log(event);
}
});
// Right-click menu
this.addMenu4Node(div.id);
}
/**
* Delete edge
*/
removeEdge() {
let cons = this.jsPlumbInstance.getConnections('*', {source: 'flowchartWindow1', target: 'flowchartWindow2'});
this.jsPlumbInstance.deleteConnection(cons);
}
}
const anchors = [[1, 0.2, 1, 0], [0.8, 1, 0, 1], [0, 0.8, -1, 0], [0.2, 0, 0, -1]];
const connectorPaintStyle = {
strokeWidth: 2,
stroke: '#61B7CF',
joinstyle: 'round',
outlineStroke: 'white',
outlineWidth: 2
},
// .. and this is the hover style.
connectorHoverStyle = {
strokeWidth: 3,
stroke: '#216477',
outlineWidth: 5,
outlineStroke: 'white'
},
endpointHoverStyle = {
fill: '#216477',
stroke: '#216477'
},
// the definition of source endpoints (the small blue ones)
sourceEndpoint = {
endpoint: 'Dot',
paintStyle: {
stroke: '#7AB02C',
fill: 'transparent',
radius: 7,
strokeWidth: 1
},
isSource: true,
connector: ['Flowchart', {stub: [40, 60], gap: 10, cornerRadius: 5, alwaysRespectStubs: true}],
connectorStyle: connectorPaintStyle,
hoverPaintStyle: endpointHoverStyle,
connectorHoverStyle: connectorHoverStyle,
dragOptions: {},
overlays: [
['Label', {
location: [0.5, 1.5],
label: 'Drag',
cssClass: 'endpointSourceLabel',
visible: false
}]
]
},
// the definition of target endpoints (will appear when the user drags a connection)
targetEndpoint = {
endpoint: 'Dot',
paintStyle: {fill: '#7AB02C', radius: 7},
hoverPaintStyle: endpointHoverStyle,
maxConnections: -1,
dropOptions: {hoverClass: 'hover', activeClass: 'active'},
isTarget: true,
overlays: [
['Label', {location: [0.5, -0.5], label: 'Drop', cssClass: 'endpointTargetLabel', visible: false}]
]
};
Summary
jsPlumb
is relatively mature compared to other drawing solutions. If you don’t want to reinvent the wheel, using this can meet your needs. However, in practical use, I personally think the official documentation is still not perfect - sometimes it’s confusing, and sometimes it contains errors.
For example, the function for deleting edges is actually called deleteConnection
, as shown below: