Skip to main content

Data Model

We recommend reading through this page in its entirety, to get a solid understanding of the data model that powers JsPlumb. Everything else in the documentation will make more sense if you're across the concepts in this page.

Basic model objects - jsPlumb Toolkit, build diagrams and rich visual UIs fast

The core model abstraction in JsPlumb is that of a graph, or more properly, a directed graph, as discussed here on Wikipedia.

A graph is a collection of nodes, groups, edges and ports. In the above screenshot - from our groups feature demonstration - we see groups, nodes and edges identified.

  • Nodes map to entities in your data model.
  • Groups are collections of nodes/groups.They map to entities in your data model.
  • Ports are points on your nodes/groups that are the endpoint of some relationship with another node/group, or with a port on another node/group.
  • Edges are relationships between nodes, groups or ports.
tip

In the JsPlumb documentation, and in the apidocs, you'll see references to Vertex and vertices - this is a node, port or group. We often use "vertex" to refer to the source and target terminus for an edge throughout the documentation as it's easier than typing "Node/Group/Port"...and also, conceptually, its correct!

Example - Schema Builder

As an example of how these data types map to an application - in the Schema Builder starter application - nodes, edges and ports are mapped as follows:

  • Nodes are tables (or views) in a database schema
  • Ports are columns on a table node
  • Edges are relationships between columns on two table nodes

Here we see the Book table, which is modelled as a node, with three columns id, isbn and title, which are modelled as ports on the Book node:

Database schema builder table node - jsPlumb Toolkit, flowcharts, chatbots, bar charts, decision trees, mindmaps, org charts and more

Example - Flowchart Builder

In the Flowchart Builder application,nodes, edges and ports are mapped as follows:

  • Nodes are objects in the flowchart such as questions or actions.
  • Ports are used to model the yes/no connection points from a Question. Action nodes do not have ports as they have only one output and this is assigned to the node itself.
  • Edges are the control flow of the chart.

Every application using JsPlumb will have its own mapping of concepts to nodes, groups, edges and ports. Some applications may not even use ports at all, since every node can be an edge target and/or source. ports just give you the ability to model more complex data structures.

Paths

In addition to nodes, groups, ports and edges, JsPlumb has the concept of a Path: an in-order list of edges and vertices that represent the path from one vertex to some other vertex. These are a very useful way of querying, and operating on, your model. Paths are discussed in a separate page.


Data Format

The various parts of your data model can be represented as any valid Javascript type. As an example, here is the backing data for the Book table in the Schema Builder starter app:

{
"id":"book",
"name":"Book",
"type":"table",
"columns":[
{ "id":"123", "name":"id", "datatype":"integer", "primaryKey":true },
{ "id":"456", "name":"isbn", "datatype":"varchar" },
{ "id":"789", "name":"title", "datatype":"varchar" }
]
}

JsPlumb has its own internal representation of its data members, but your original data is always stored under the data member - this is the Node from above:

{
"id":"456",
"data":{
"id":"book",
"name":"Book",
"type":"table",
"columns":[
{ "id":"123", "name":"id", "datatype":"integer", "primaryKey":true },
{ "id":"456", "name":"isbn", "datatype":"varchar" },
{ "id":"789", "name":"title", "datatype":"varchar" }
]
}
}

Port Data

In the node data given above there is a columns array - each entry in this array maps to a port on the node representing that table. The IDs of the ports here are the id members from each column in the array.


Object IDs

Every vertex is required to have a unique ID. JsPlumb attempts to derive this automatically from your data, by looking for an id member - which should be a string.

Should you wish to implement a different strategy, though, you can just supply your own idFunction to the method you use to get an instance of JsPlumb:

Vanilla JS
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const jsplumb = newInstance({
idFunction:(data:ObjectData):string {
return SomeCustomComputing(data);
}
})

Again note this is optional - you do not need to supply this function, but if you do not then JsPlumb will expect an id member in your data. Remember to pass back the ID as a string. This method will be used to attempt to derive an ID for any model object.

Group IDs

IDs for groups are derived using whatever method JsPlumb is using to derive IDs for nodes - either by looking for an id value in your data, or by using a supplied idFunction. group IDs must be unique across all groups and nodes in the dataset: you cannot have a group that has the same ID as some node.

Edge IDs

You are not required to supply an ID for every edge, but if you do not, JsPlumb will assign one automatically.

Port and edge ID functions

You can supply separate functions to use for extracting the ID for a port or edge from some backing data:

import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
idFunction:(data:ObjectData) => {
return SomeCustomComputing(data)
},
portIdFunction:(data:ObjectData) => {
return portComputations(data)
},
edgeIdFunction:(data:ObjectData) => {
return edgeComputations(data)
}
})

If you omit either of these, JsPlumb uses whatever it is using to derive node IDs (either the idFunction, if provided, or the default mechanism of looking for an id member).

Port ID Uniqueness

Port IDs are required to be unique on the node on which the port exists, but may be the same as the ID of a port on some other node (and in fact this is quite common).

Referencing ports by ID

When adding an edge to a JsPlumb instance, you can reference a port on some node using, by default, dotted notation. In the data given above there were three ports. We could connect one of them to a column on another table like this:

toolkit.connect({
source:"book.id",
target:"book_author.book_id"
})

book_author is another table in the schema from the Schema Builder starter app.

Custom port ID Separator

If you find that using a period as the separator in a port ID does not work for your data model, you can override what JsPlumb will use by doing this:

import { newInstance } from "@jsplumbtoolkit/browser-ui"

const tk = newInstance({
portSeparator:"#"
})

Extracting and synchronizing port data

When you load data into an instance of JsPlumb and you have nodes/groups that themselves have ports (consider the Schema Builder starter app - a table is a node, and the columns on that table are ports), the data describing your ports will typically be held within the node data. For instance, in the Schema Builder, a table node looks like this:

{
"id":"book",
"name":"Book",
"type":"table",
"columns":[
{ "id":"123", "name":"name", "type":"varchar" },
{ "id":"456", "name":"id", "type":"integer" }
]
}

The entries in the columns array are the ports for this node. In order to get JsPlumb to recognise these ports when using the load function, you should define a portExtractor when you create an instance of the Toolkit:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance,
ObjectData,
Node,
Group } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"portExtractor": (data:ObjectData, parent:Node|Group):Array<ObjectData> => {
return data.columns || [];
}
})

const surface = toolkit.render(someElement, {});

Synchronizing node data with port data

A further consideration with the setup discussed above is how to go about keeping the original data, now stored in the node/group, in sync with the reality of the current ports and their backing data. To do this, you can specify a portUpdater function in the arguments to the getInstance call:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance,
ObjectData,
Node,
Group,
Port } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"portUpdater": (data:ObjectData, parent:Node|Group, ports:Array<Port>) => {
return Object.assign(data, {
columns:ports.map((p) => p.data)
})
}
})

const surface = toolkit.render(someElement, {});

data is the node's current data. node is the node itself, and ports is an array of port objects. This example is from the original version of the Database Visualizer demo, which now uses the portDataProperty described below as it is a simple use case). In practise we find that people using a library integration need only define a portDataProperty, since their library manages keeping the data up to date.

Simple synchronisation

In many cases the portExtractor and the portUpdater end up being a matched pair: the extractor returns some array property, and the updater simply extracts the data from each port and returns it in an object whose key is that same property name and whose value is an array:

import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
portExtractor:function(data, node) { return data.columns || []; },

portUpdater:function(data, node, ports) {
return jsPlumb.extend(data, {
columns:jsPlumbToolkitUtil.map(ports, function(p) { return p.data; })
});
}
})
Vanilla JS
React
Angular
Vue
Svelte
import { newInstance,
ObjectData,
Node,
Group,
Port } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"portExtractor": (data, node) => { return data.columns || []; },
"portUpdater": (data, node, ports) => {
return Object.assign(data, {
columns:ports.map(p => p.data)
});
}
})

const surface = toolkit.render(someElement, {});

In these situations you can replace both functions by declaring a portDataProperty:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"portDataProperty": "columns"
})

const surface = toolkit.render(someElement, {});

This will cause JsPlumb to create the portExtractor and portUpdater functions shown above. Note that this mechanism assumes all nodes in your dataset have port data keyed by this property. For nodes that don't have ports this is not a problem: an empty array will be returned and no ports will be registered.


Port Ordering

In some situations you may wish to specify the order of ports on a node (typically because it will help you render the UI the way you want). If you're using the portDataProperty to tell JsPlumb which property holds port data, you can also specify a portOrderProperty, which provides the ordering of the ports on a node. To take the previous example and add ordering:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"portDataProperty": "columns",
"portOrderProperty": "order"
})

const surface = toolkit.render(someElement, {});

Given a node whose backing data looks like this:

{
"name":"A Table",
"id":"123456",
"columns":[
{ "name":"Type", "order":2, "id":"5234532" },
{ "name":"Nullable", "order":3, "id":"78947021" },
{ "name":"ID", "order":1, "id":"76486213" }
]
}

Then with the JsPlumb instance created above, the ports would be reordered by JsPlumb into the order ID, Type, Nullable.


Object type

Every object has an associated type. This is an important concept in JsPlumb, as it is the basic means by which the data model is bound to any renderers, via Views.

As with IDs, the type of a model object can be derived using a default function - which looks for a type member in the backing data, or you can supply a typeFunction to the newInstance method:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance,
ObjectData } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"idFunction": (data:ObjectData) => {
return SomeCustomComputing(data);
},
"typeFunction": (data:ObjectData) => {
return SomeOtherComputing(data);
},
"edgeTypeFunction": (data:ObjectData) => {
return edgeTypeComputing(data);
},
"portTypeFunction": (data:ObjectData) => {
return portTypeComputing(data);
}
})

const surface = toolkit.render(someElement, {});

As with ID, omitting either the portTypeFunction or edgeTypeFunction will cause JsPlumb to use whatever function it has determined it should use for deriving node types. Also, as with ID, JsPlumb will derive group types via the same means it is using to derive node types.

The type of some model object is used by the view in a surface, to determine the appearance and behaviour of the object. For a discussion of views and rendering to a surface, see the surface and views documentation.

To change the type of some object after it has been initially created, use the setType method on a Toolkit instance. For a discussion, see this page


Data Factories

Node factory

When you need to add a new node programmatically, you do so on the JsPlumb instance using the addNode function:

addNode : function(data) {
...
}

It is expected that the current idFunction and typeFunction will be able to determine appropriate values from the data you provide. An example might be something like the following:

toolkit.addNode({
id:"place",
name:"Place",
columns:[
{ id:"id", name:"Id", primaryKey:true, datatype:"integer" },
{ id:"name", name:"Name", datatype:"varchar" },
{ id:"lat", name:"Latitude", datatype:"float" },
{ id:"lng", name:"Longitude", datatype:"float" }
]
})

If, however, you are using a drag and drop mechanism to add new nodes to the UI (for instance via a SurfaceDropManager), JsPlumb needs some way of getting the data for a new node. By default, JsPlumb will create a JS object with an id member set to a new UUID, but you can provide an ObjectFactory in order to gain control over the creation of the data.

An object factory is a function with this signature:

export declare type ObjectFactory = (type: any, data: any, continueCallback: Function, abortCallback: Function, params?: any) => boolean;
  • type is the type of object to create.
  • data is the data appropriate to the given object type; in the case of drag and drop it has been generated elsewhere (for instance, via a SurfaceDropManager).
  • callback is a function that your code must call once it has the data for the new object prepared - having a callback rather than returning a value enables you to optionally prepare the data asynchronously, perhaps via a round trip to the server. If you do not call this function, addition of the new object is aborted (which you might want to do, sometimes, of course).
  • originalEvent This is the browser event related to the drop. If isNative is set, you might wish to access the dataTransfer member of this event when deciding how to populate the data object.
  • isNative Set to true if the drop event was "native", for example a file dragged into the browser from the user's desktop or file system.

Direct callback

Here's an example of a direct callback:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"nodeFactory": (type, data, callback, evt, native) => {
callback({
someKey:\aValue\",
someArray:[ 1, 2, 3 ]
});
}"
})

const surface = toolkit.render(someElement, {});

Ajax callback

You might do this instead if you want to get your new node from the server:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"nodeFactory": (type, data, callback, evt, native) => {
makeAnAjaxCall({
url:\/get/new/\" + type,
success:callback
});
}"
})

const surface = toolkit.render(someElement, {});

Native drop

You might do this if it's a native drop:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"nodeFactory": (type, data, callback, evt, native) => {
if (isNative) {
data.name = evt.dataTransfer.files[0].name;
data.size = evt.dataTransfer.files[0].size;
data.type = evt.dataTransfer.files[0].type;
}
callback(data);
}
})

const surface = toolkit.render(someElement, {});

Calling the node factory directly

You can call the node factory directly using the addFactoryNode method:

toolkit.addFactoryNode("someType");

This will result in a call to the node factory with the type someType.

It is possible to also provide some seed data for the new node:

toolkit.addFactoryNode("someType", { foo:"bar" });

And also you can provide a callback which will be run after the node factory has finished adding the new node:

toolkit.addFactoryNode("someType", { foo:"bar" }, function(node) {
// node is your new node.
});

Accessing the new object

The callback function provided to node/group factories returns the newly created object. Taking the example from above, for instance, we can assign the return value of callback:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"nodeFactory": (type, data, callback, evt, native) => {
const newNode = callback({
someKey:\aValue\",
someArray:[ 1, 2, 3 ]
});
}"
})

const surface = toolkit.render(someElement, {});

This can be useful in certain situations. We were building an application for a client who wanted to edit newly dropped nodes as soon as they were dropped, so we initially set it up like this:

nodeFactory: function (type, data, callback) {

_editNode(data, true, (atts, cancelled) => {
if (!cancelled) {
callback(atts);
}
});

}

Here, _editNode opens up a node inspector in a side panel, and after editing hits a callback with the data and a flag indicating whether the user cancelled the dialog.

The drawback with this approach was that the new node did not appear on the canvas while the user was editing the details. So we rewrote our first pass to be this:

nodeFactory:  (type, data, callback) => {

var newNode;

setTimeout(() => {
_editNode(data, true, (atts, cancelled) => {

if (cancelled && newNode != null) {
toolkit.removeNode(newNode);
} else {
toolkit.updateNode(newNode, atts);
}
});
}, 0);

newNode = callback(data);
}

We assign the return value of the callback to newNode, and set a timeout to call the node editor. The user sees the new node on the canvas and in the editor, and then the edit handler makes the decision about whether to remove the new node, or to update it with its newly entered information.

Group factory

A group factory works in exactly the same way as a node factory. Here's the setup of JsPlumb in the available on Github, the source code for which is available on Github.

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"groupFactory": (type, data, callback) => {
data.title = \Group \" + (toolkit.getGroupCount() + 1);
callback(data);
}",
"nodeFactory": (type, data, callback) => {
data.name = (toolkit.getNodeCount() + 1);
callback(data);
}
})

const surface = toolkit.render(someElement, {});

In this app we simply assign a label that has the group/node index in it.

Note that the group factory's callback method will return the newly created group.

Port factory

If your data model is sufficiently complex, you will be making use of ports. In the same way that JsPlumb needs to be able to assemble a JS object for each new node created through user interaction, it needs a way to assemble a JS object for a new port.

JsPlumb offers two methods for adding new ports:

addPort(nodeOrGroup:Node|Group, data:Record<string, any>)

and

addNewPort(nodeOrGroup:Node|Group, type:string, portData:Record<string, any>)

The first of these - addPort - takes some existing data as argument, from which it will derive an ID and type (from the current idFunction and typeFunction) and then add the port to the given node or group.

The second function takes the type of the port to add, which it hands off to the port factory in order to get a data object for the new port. As with nodes, if no portFactory is defined then a simple JS object containing an id parameter is created.

Here's the port factory from the Schema builder example application. In this application, each column in a table is mapped to a port:

function(node, type, data, callback) {
let column = {
id:data.columnName,
name:data.columnName[0].toUpperCase() + data.columnName.slice(1),
datatype:"varchar",
type:"column"
};
// add to node's data. we have to do this manually. JsPlumb does not know our internal
// data structure.
node.data.columns.push(column);
// handoff the new column.
callback(column);
}
note

The signature is slightly different for a port factory: it is given the node/group to which the port should be added, and it is not given a browser event or the isNative flag.

addNewPort takes three values:

  • The node to add the port to (either an ID or a node itself)
  • The type of port to add
  • A JS object containing data for the new port.

Edge Factory

The edge factory is the function that JsPlumb will call whenever a new edge is established between two nodes/ports, via user mouse action. It follows the same pattern as nodes and ports - if there is no edge factory defined then a default JS object will be used. The edge factory is only ever called when the user creates a new edge with the mouse. It is not called if you make a programmatic call toaddEdge.

Edge factory context

The edge factory is passed a context value as the 4th argument to the function. It contains the following:

{
sourceNodeId : ID of the node that is the source of the edge (or the node on which the source port resides)
sourcePortId : ID of the port that is the source of the edge. Null when the source is not a port.
targetNodeId : ID of the node that is the target of the edge (or the node on which the target port resides)
targetPortId : ID of the port that is the target of the edge. Null when the target is not a port.
type : Type of the edge being created
source : The edge source - a node, group or port.
target : The edge target - a node, group or port.
sourceId : Full ID of the edge source. When the source is a port this will be in dotted (node.port) format
targetId : Full ID of the edge target. When the target is a port this will be in dotted (node.port) format
sourceType : "Group", "Node", or "Port". This is fact just the `objectType` property from the value of `source`.
targetType : "Group", "Node", or "Port". This is fact just the `objectType` property from the value of `target`.
}


Constraining Connectivity

note

This concept is also discussed in the dragging edges docs, but it's important to note that interceptors discussed here run at the model level, ie. in response both to UI events and also to calls to the programmatic API.

Connectivity can be controlled at runtime by interceptors - callbacks that can be used to cancel some proposed activity, and that are bound on an instance of the Toolkit by supplying a specific function in the Toolkit constructor options. JsPlumb currently supports five interceptors.

beforeConnect

A function to run before an edge with the given data can be established between the given source and target. Returning false from this method aborts the connection. Note that this method fires regardless of the source of the new edge, meaning it will be called when loading data programmatically.

Method signature

beforeConnect(source: Vertex, target: Vertex): any

Parameters

  • source The source vertex for the new edge
  • target The target vertex for the new edge

Example

Here, we reject connections from any vertex to itself.

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeConnect": (source: Vertex, target: Vertex) => {
return (source !== target)
}
})

const surface = toolkit.render(someElement, {});

beforeMoveConnection

A function to run before an edge of the given type is relocated from its current source or target to a new source or target. Returning false from this method will abort the move.

Method signature

beforeMoveConnection(source: Vertex, target: Vertex, edge: Edge): any

Parameters

  • source Candidate source. May be the edge's current source, or may be a new source.
  • target Candidate target. May be the edge's current target, or may be a new target.
  • edge The edge that is being moved.

The parameters source and target reflect the source and target of the edge if the move were to be accepted. So if, for example, your user drags a connection by its target and drops it elsewhere, target will be the drop target, not the edge's current target, but source will be the edge's current source. You can access the current source/target via the source and target properties of edge.

Example

Here, we reject moving any edge that has fixed:true in it backing data:

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeMoveConnection": (source: Vertex, target: Vertex, edge:Edge) => {
return (edge.data.fixed !== true)
}
})

const surface = toolkit.render(someElement, {});

beforeStartConnect

A function to run before an edge of the given type is dragged from the given source (ie. before the mouse starts moving). This interceptor is slightly different to the others in that it's not just a yes/no question: as with the other interceptors, returning false from this method will reject the action, that is in this case it will not allow a connection drag to begin. But you can also return an object from this method, and when you do that, the connection start is allowed, and the object you returned becomes the payload for the new edge.

Method signature

beforeStartConnect(source: Vertex, type: string): any

Parameters

  • source The vertex that is the source for the new edge
  • type The computed type for this new edge.

Example - reject a connection start

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeStartConnect": (source: Vertex, type:string) => {
return type !== 'not-connectable'
}
})

const surface = toolkit.render(someElement, {});

Example - provide an initial payload

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeStartConnect": (source: Vertex, type:string) => {
return {
type,
message:`initial payload for vertex ${source.id}`
}
}
})

const surface = toolkit.render(someElement, {});

beforeDetach

A function to run before the given edge is detached from the given source vertex. If this method returns false, the detach will be aborted.

Method signature

beforeDetach(source: Vertex, target: Vertex, edge: Edge, isDiscard?: boolean): any

Parameters

  • source The source vertex for the edge that is to be detached.
  • target The candidate target for the edge - may be null, if the edge is being discarded
  • edge The edge that is being detached.
  • isDiscard True if the edge is not now connected to a target.

Example

Here, we reject the detach if the target is null, ie. the user is trying to discard the edge, not relocate it.

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeDetach": (source: Vertex, target: Vertex, edge: Edge, isDiscard?: boolean) => {
return target != null
}
})

const surface = toolkit.render(someElement, {});

beforeStartDetach

Method signature

beforeStartDetach(source: Vertex, edge: Edge): any

A function to run before the given edge is detached from the given source vertex. If this method returns false, the detach will be aborted. The difference between this and beforeDetach is that this method is fired as soon as a user tries to detach an edge from an endpoint in the UI, whereas beforeDetach allows a user to detach the edge in the UI.

Parameters

  • source The source vertex for the edge that the user has started to detach
  • edge The edge that the user has started to detach

Example

Here, we reject the detach if the source vertex has doNotDetachEdges:true in its backing data.

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeDetach": (source: Vertex, edge: Edge) => {
return source.data.doNotDetachEdges !== true
}
})

const surface = toolkit.render(someElement, {});

Example

In this example we provide a beforeStartConnect and beforeDetach interceptor to an instance of the Toolkit. The beforeStartConnect interceptor prevents the user from dragging connections from any vertex whose ID is not an even number. The beforeDetach interceptor reattaches detached connections whose source ID is not an event number

Vanilla JS
React
Angular
Vue
Svelte
import { newInstance } from "@jsplumbtoolkit/browser-ui"

const toolkit = newInstance({
"beforeStartConnect": (source, type) => {
// only allow connections from nodes whose
// ID is an even number
return parseInt(source.id, 10) % 2 === 0
},
"beforeDetach": (source, target, edge, isDiscard) => {
// only allow connections to be detached whose
// source ID is an even number
return parseInt(edge.source.id, 10) % 2 === 0
}
})

const surface = toolkit.render(someElement, {});
Interceptors