Skip to main content

· 23 min read
Simon Porritt

Recently we had the idea that a good addition to our list of starter applications might be a chatbot demo. In this post we're going to track the process in real time, or real-ish time, at least. We're going to write this in ES6, and we'll use esbuild to create our bundle.

The source code of this demo is available on Github. If you're not a licensee of the Toolkit, you can start your 30 day free trial of jsPlumb here.

So - here goes.

8:10am - initialisation

Create a new folder for this app and initialise npm

mkdir chatbot
cd chatbot
npm init

Install esbuild, Babel and jsPlumb Toolkit

npm i --save-dev esbuild babel-cli babel-preset-env

npm i @jsplumbtoolkit/browser-ui

Add a few build tasks

We're building to ES6, both minified and unminified, and also to ES5 (via Babel)

"scripts": {
"build-es6": "./node_modules/.bin/esbuild demo.js --target=es2016 --bundle --format=iife --outfile=bundle.js",
"build-es6-min": "./node_modules/.bin/esbuild demo.js --target=es2016 --bundle --minify --format=iife --outfile=bundle-min.js",
"transpile-es5": "./node_modules/.bin/babel bundle.js -o bundle-es5.js",
"transpile-es5-min": "./node_modules/.bin/babel bundle-min.js -o bundle-min-es5.js",
"build": "npm run build-es6;npm run transpile-es5",
"build-min": "npm run build-es6-min;npm run transpile-es5-min",
"serve": "./node_modules/.bin/http-server ."
}

8:15am - create structure

Create index.html, demo.js and app.css

The head of our index.html will include the Toolkit's default CSS file, as well the CSS for the controls component - more on which later.

<head>
<title>jsPlumbToolkit - Chatbot builder</title>
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1" />
<meta http-equiv="content-type" content="text/html;charset=utf-8" />
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css">
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css">
<link rel="stylesheet" href="./app.css">
</head>

In the body we've got a basic structure of a main canvas, plus placeholders for our miniview and controls component, and then a sidebar from which we will drag new nodes onto the canvas.

<body>

<div class="jtk-demo-main" id="jtk-demo-chatbot">

<!-- main drawing area -->
<div class="jtk-demo-canvas">
<!-- controls -->
<div class="jtk-controls-container"></div>
<!-- miniview -->
<div class="miniview"></div>
</div>
<div class="jtk-demo-rhs">

<!-- the node palette -->
<div class="sidebar node-palette"></div>
<!-- node/edge inspector -->
<div class="inspector"></div>
</div>


</div>

<!-- the demo code -->
<script src="bundle.js"></script>

</body>

8:25am - create demo.js and run an initial build

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


ready(() => {

const canvas = document.querySelector(".jtk-demo-canvas")
const palette = document.querySelector(".node-palette")
const inspector = document.querySelector(".inspector")

const toolkit = newInstance()

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
}
})

})
npm run build

...and, we have this:

A blank screen! Excellent. That makes sense - we haven't configured anything to render and we havent loaded any data. So let's do that.


8:35am - add start/end nodes

Our chatbot builder is going to allow our users to create a flow from a start node, through several actions, to an end node. We'll start by adding support for start and end nodes. To do that, we need to:

  1. create a template that will render start nodes
  2. create a template that will render end nodes
  3. map these templates in our view so that the Toolkit knows when to use them
  4. create a node palette that can render these and allow a user to drag them on to the canvas.

First we'll create a couple of constants to identify these node types:

const START = "start"
const END = "end"

Then we'll create a template for each one in the view:


const START = "start"
const END = "end"

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
},
view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start">START</div>`
},
[END]:{
template:`<div class="jtk-chatbot-end">END</div>`
}
}
}
})
note

You don't have to provide node templates directly in your view like this. If you have a more complex template you can embed it in your HTML page or include it in a separate HTML page.

Let's also just add one of each of these to the Toolkit when we start up the app so we can have a look at them:

toolkit.load({
data:{
nodes:[
{ id:"1", type:"start", left:0, top:0 },
{ id:"2", type:"end", left:0, top:250 }
]
}
})

Let's have a look now:

A little plain. I think we'll make these nodes circles with a solid color and white text. We can change it up later easily enough as it's all just handled in the CSS:

.jtk-chatbot-start, .jtk-chatbot-end {
width:80px;
height:80px;
border-radius:50%;
display:flex;
color:white;
justify-content: center;
align-items: center;
text-transform: uppercase;
}

.jtk-chatbot-start {
background-color:navy;
}

.jtk-chatbot-end {
background-color:#ff667b;
}

.jtk-chatbot-start::after {
content:'START';
}

.jtk-chatbot-end::after {
content:'END';
}

and also, let's have the Toolkit center the content after it loads it, via the zoomToFit render option:

const surface = toolkit.render(canvas, {
layout:{
type:AbsoluteLayout.type
},
view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start"></div>`
},
[END]:{
template:`<div class="jtk-chatbot-end"></div>`
}
}
},
zoomToFit:true
})

Not bad:

We've just done steps 1,2 and 3 outlined above. Now to add the node palette. We're going to use a SurfaceDropManager for this - a component that hooks up to a Surface and lets you render a list of items you'd like to drop onto the Surface. The code to initialise our drop manager looks like this:

const dropManager = new SurfaceDropManager({
surface,
source: palette,
selector: ".jtk-chatbot-palette-item",
dataGenerator:(el) => {
return {
type:el.getAttribute("data-type")
}
}
})

We've given it four pieces of information:

  1. The Surface to attach to
  2. The element in which the draggable nodes will be found
  3. A CSS selector that identifies draggable nodes in that element
  4. A function that it can use to generate an initial payload for any node that is dragged

We've also updated our HTML to include a draggable item for start and end nodes:

<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
<div class="jtk-chatbot-start"></div>
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
<div class="jtk-chatbot-start"></div>
</div>
</div>

And now our UI looks like this:

Getting there!


9:03am - adding action nodes

What we want to do with this app is provide the ability for the chatbot to say things to our users, and to perhaps gather responses from the user before moving on to the next step. We're going to support, for now, 3 different types of actions:

  • Chatbot tells the user something and does not wait for a response, moving on to the next step
  • Chatbot tells the user something and offers a set of responses from which the user needs to select one
  • Chatbot tells the user something and waits for a free-form response from the user

The Data Model

We always recommend to people that a clear concept of the data model behind an application is key to its success. This application consists of a series of steps, and we're representing each step as a Node. But how can we represent the transitions to some other step in our data model? When we send a message and don't wait for a response, or when the user enters some free-form text, the transition is simple: go from this step to the next step - no decision about the next step is required. But if we want to provide the user with some choices then our data model is a little more complex, and in the Toolkit we generally use ports for this. As our documentation says:

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.

Let's put the cart slightly ahead of the horse and sketch out the action nodes. First we'll add constants to represent their data types:

const ACTION_MESSAGE = "message"
const ACTION_INPUT = "input"
const ACTION_CHOICE = "choice"

Then we'll create initial templates for them and map them in our view:

view:{
nodes:{
[START]:{
template:`<div class="jtk-chatbot-start"></div>`
},
[END]:{
template:`<div class="jtk-chatbot-end"></div>`
},
[ACTION_MESSAGE]:{
template:`<div class="jtk-chatbot-message">{{message}}</div>`
},
[ACTION_INPUT]:{
template:`<div class="jtk-chatbot-input">
{{message}}
<textarea rows="5" cols="10" placeholder="{{prompt}}"/>
</div>`
},
[ACTION_CHOICE]:{
template:`<div class="jtk-chatbot-choice">
{{message}}
<r-each in="choices" key="id">
<span>{{label}}</span>
</r-each>
</div>`
}
}
}

Now let's add them to our initial dataset so we can take a look:

toolkit.load({
data:{
nodes:[
{ id:"1", type:"start", left:0, top:0 },
{ id:"3", type:"message", left:-150, top:250, message:"Hi! I am a chatbot" },
{ id:"4", type:"input", left:0, top:250, message:"Please enter your name:", prompt:"your name, please!"},
{
id:"5",
type:"choice",
left:250, top:250,
message:"Make a choice",
choices:[
{ id:"choice1", label:"Choice 1" },
{ id:"choice2", label:"Choice 2" },
]
},
{ id:"2", type:"end", left:0, top:650 }
]
}
})

Hmmm, not bad. Clearly it's time for some more CSS!

.jtk-chatbot-message, .jtk-chatbot-input, .jtk-chatbot-choice {
color:white;
display:flex;
padding:1rem;
border-radius:5px;
}

.jtk-chatbot-message {
width:80px;
background-color: mediumseagreen;
justify-content: center;
align-items: center;
text-transform: uppercase;
}

.jtk-chatbot-input {
min-width:150px;
background-color: coral;
flex-direction: column;
}

.jtk-chatbot-input textarea {
margin-top:5px;
resize: none;
}

.jtk-chatbot-choice {
min-width:150px;
background-color: rebeccapurple;
flex-direction: column;
}

.jtk-chatbot-choice-option {
display:flex;
align-items: center;
margin:3px 0;
background-color: palevioletred;
padding:3px;
border-radius:3px;
}

And we have this:

It's starting to come together now. We'd like to be able to drag those action items from the node palette, though, so we'll update the HTML and add them:

<div class="sidebar node-palette">
<div class="jtk-chatbot-palette-item" data-type="start">
<div class="jtk-chatbot-start"></div>
</div>
<div class="jtk-chatbot-palette-item" data-type="end">
<div class="jtk-chatbot-end"></div>
</div>
<div class="jtk-chatbot-palette-item" data-type="message">
message
</div>
<div class="jtk-chatbot-palette-item" data-type="input">
input
</div>
<div class="jtk-chatbot-palette-item" data-type="choice">
choice
</div>
</div>

We also need to update the code that generates a payload for each of these new action types:

dataGenerator:(el) => {
const type = el.getAttribute("data-type")
const base = { type }
if (type === ACTION_MESSAGE) {
Object.assign(base, { message:"Send a message"})
} else if (type === ACTION_INPUT) {
Object.assign(base, { message:"Grab some input", prompt:"please enter input"})
} else if (type === ACTION_CHOICE) {
Object.assign(base, {
message:"Make a selection!",
choices:[
{ id:"1", label:"Choice 1"},
{ id:"2", label:"Choice 2"},
]
})
}

return base
}

The result:

I've helpfully dragged out one of each action node type onto the canvas for your enjoyment. Obviously the styles there might need to be revisited, but that's fine, it'll just be a little bit of CSS. And maybe a little bit of HTML.


9:45am - establishing connectivity

So far we have the ability to drag start, end and action nodes onto a canvas and to render them. We can also load up a dataset. But we can't yet connect anything. As I mentioned above, we're representing each step as a node, and on the CHOICE node type we're representing each choice as a port.

To setup visual connectivity, we need to decide on two main things:

What are the rules governing how steps can be connected?

  • We can only drag from START nodes
  • We can only drag to END nodes
  • We can drag to and from MESSAGE nodes
  • We can drag to and from INPUT nodes
  • We can drag to a CHOICE node, but not to the individual choices
  • We can drag from the choices in a CHOICE node, but not from the node itself

How exactly do we want the user to interact with our UI to establish edges?

There's a key tradeoff with this sort of application: how can we allow a node to be draggable but also allow a user to drag from it? The Toolkit allows us to specify particular parts of some node's element that can act as edge sources and/or targets. Let's take the START node as an example - we could, theoretically, make it non-draggable and then use the whole node as the edge source. But we don't want to do that in this app, so we're going to add an element to its markup and designate it as an edge source:

[START]:{
template:`<div class="jtk-chatbot-start">
<div class="connect" data-jtk-source="true"/>
</div>`
}

The data-jtk-source attribute is how we tell the Toolkit that the given element is somewhere from which the user can drag edges. We'll style that a little in our CSS:

.connect {
width: 15px;
height: 15px;
background-color: #e8e8b2;
border-radius: 50%;
cursor: pointer;
position: absolute;
bottom: -0.25rem;
left: 50%;
margin-left: -7.5px;
}

Great! So let's add that to the MESSAGE and INPUT nodes now. We don't need to add it to END nodes, because they can only accept incoming connections, meaning we can use the whole element. And we're not adding it to CHOICE nodes, because only their individual choices can be the source of connections.

[ACTION_MESSAGE]:{
template:`<div class="jtk-chatbot-message">
{{message}}
<div class="connect" data-jtk-source="true"/>
</div>`
},
[ACTION_INPUT]:{
template:`<div class="jtk-chatbot-input">
{{message}}
<textarea rows="5" cols="10" placeholder="{{prompt}}"/>
<div class="connect" data-jtk-source="true"/>
</div>`
}

Configuring choices as edge sources

To setup each choice as an edge source, we alter the markup as follows:

<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option"
data-jtk-source="true"
data-jtk-port="{{id}}">{{label}}</div>
</r-each>

data-jtk-source tells the Toolkit that the whole element is an edge source. data-jtk-port tells the Toolkit the ID of the port that the edge is coming from. So imagine we had this CHOICE node, like from our initial dataset:

{
id:"5",
type:"choice",
left:250, top:250,
message:"Make a choice",
choices:[
{ id:"choice1", label:"Choice 1" },
{ id:"choice2", label:"Choice 2" },
]
}

When you drag an edge from Choice 1, the Toolkit uses the ID 5.choice1, meaning "the port with ID 'choice1' on the node with ID '5'". If we dragged an edge from Choice 1 to the END node now, in the model we'd have:

{
source:"5.choice1", target:"2"
}

because "2" is the ID of the END node above.

Configuring edge targets

We've got all our edge sources configured, now we'll setup the targets, based on the rules given above. In a nutshell, every node except the START node can be an edge target. This is straightforward to setup in the Toolkit - we simple add a data-jtk-target attribute to the template for each node that should be a target. For instance, here's the END node template now:

[END]:{
template:`<div class="jtk-chatbot-end" data-jtk-target="true"></div>`
}

We've done the same thing on ACTION, INPUT and CHOICE nodes, but not START of course.

At this point, we can drag some edges:

it doesn't look very pretty, though - what's going on? Three things:

  • We need to decide if we want an endpoint on each edge or not. I don't want an endpoint. I'm going to use a Blank endpoint - see below.
  • We need to come up with a nice appearance for our edges. We can do this programmatically, by supplying a style object to the Surface, or we can do it with CSS. We'll use CSS - it's simple and easy to change.
  • We need to think about where on each node we want connections to be attached. We call these anchors in the Toolkit. Configuring your anchors can make a significant difference to the appearance of an app using the Toolkit. So let's think about our options. Basically we're probably OK with every node being able to support connections on any side of the element. For this we can use the Continuous anchor, so let's set that up, and we'll also setup the Blank endpoint. We do this in the render parameters, in the defaults section:
toolkit.render(canvas, {
view:{ ... },
zoomToFit:true,
defaults:{
endpoint:BlankEndpoint.type,
anchor:AnchorLocations.Continuous
}
})

In the absence of any more specific instructions, the Toolkit will use a Blank endpoint and a Continuous anchor throughout the UI.

That's not bad, although it's hard to see the connector - we need to do what we mentioned above, make a style for the connector. I think we'll just use black, with a stroke width of 2 pixels. That should pop. But if you don't like it, it's easily changed!

.jtk-connector path {
stroke:black;
stroke-width: 2px;
}

Also it's a little difficult to see the flow. Let's add an arrow to the end of the edge. There are various different arrows you can use. I think here we'll go for a PlainArrow. To do this we're going to introduce a default edge definition into our view, and declare an overlay on it:

view:{
nodes: { ... },
edges:{
default:{
overlays:[
{
type:PlainArrowOverlay.type,
options:{
location:1,
width:10,
height:10
}
}
]
}
}
}

Looks pretty good. I'm a little bothered by the way the arrow goes from Choice 1 to END, though - it doesn't feel very intuitive. It would be better if it only went from the right or left hand side of the choice element, but since we're using the Continuous anchor it can choose any face - and it chooses the closest face to its target. The solution? We can define a port type for the choice elements, and map an anchor to that specific type.

First of all we update the template for CHOICE nodes to make each choice element declare a port type:

<div class="jtk-chatbot-choice-option" 
data-jtk-source="true"
data-jtk-port-type="choice"
data-jtk-port="{{id}}">{{label}}</div>

We're calling it "choice". Then we add a mapping for this port type to our view, and declare an anchor for it:

view:{
nodes:{ ... },
edges:{ ... },
ports:{
choice:{
anchor:[AnchorLocations.Left, AnchorLocations.Right ]
}
}
}

Now when we drag an edge from a choice, it is always anchored on the left or right hand side of the element:

This is known as a dynamic anchor in the Toolkit - an anchor whose position can be selected from one of a number of points. The Toolkit picks the point that is closest to the center of the element at the other end of the edge.


10:44am - stocktake.

So, what have we got so far?

  • we can drag various node types onto the canvas
  • we can connect nodes to each other based upon a set of connectivity rules
  • we can support multiple outputs from a choice node

What else would we like to do? We'd probably like to inspect our nodes and update their values.


10:45am - adding an inspector

We declared an inspector div from the outset in our HTML. The Toolkit has support for Inspectors - components that connect to an underlying Toolkit instance and allow you to edit the values of nodes and edges.

To setup an inspector we need to decide which node types can be edited. In this app we'll support editing MESSAGE, INPUT and CHOICE nodes. So we need to create a UI for each of these, exposing the fields that are editable for each type. To keep the code easy to read, let's do this in a new file, inspector.js. The code looks like this:

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

import { ACTION_INPUT, ACTION_MESSAGE, ACTION_CHOICE } from "./demo";

const PROPERTY_MESSAGE = "message"
const PROPERTY_PROMPT = "prompt"

/**
* Inspector for chatbot nodes.
*/
export class ChatbotInspector extends VanillaInspector {

inspectors = {
[ACTION_MESSAGE]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>`,
[ACTION_INPUT]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
<input type="text" jtk-att="${PROPERTY_PROMPT}" placeholder="prompt"/>
</div>`,
[ACTION_CHOICE]:`
<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_MESSAGE}" placeholder="message"/>
</div>`
}

constructor(options) {
super(Object.assign(options, {
templateResolver:(obj) => {
return this.inspectors[obj.type]
}
}))

}
}

We provide a template for each of the three node types we support. This class extends VanillaInspector, which ships with the Toolkit, and which expects a templateResolver as a constructor argument.

With this inspector available, we now need to create one, in demo.js:

new ChatbotInspector({
toolkit,
container:inspector,
surface
})

And we need to somehow trigger the inspector. Inspectors listen to changes in the underlying Toolkit's current selection, so here we add a new node type which has a tap event listener on it:

view:{
nodes:{
[SELECTABLE]:{
events:{
[EVENT_TAP]:(p) => {
toolkit.setSelection(p.obj)
}
}
},
[ACTION_MESSAGE]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-message" data-jtk-target="true">
{{message}}
<div class="connect" data-jtk-source="true"/>
</div>`
},
...
}
}

The tap event listener sets the tapped node to be the Toolkit's current selection, which the inspector detects. Note here in the ACTION_MESSAGE node definition, we added parent:SELECTABLE. Node, edge, port and group definitions in the Toolkit can declare a parent definition, which allow you to setup common behaviour. This is all discussed in our documentation on views.

And this is the result:

I added a few styles to app.css for the inspector, but I wanted to call out this style that I also added, which you can see in the screenshot above:

.jtk-surface-selected-element {
outline:2px dotted cornflowerblue;
outline-offset: 0.5rem;
}

When an element is in the Toolkit's current selection, it has the class jtk-surface-selected-element applied to it in the UI. You can use this to provide visual cues to your users.


11:13am - managing choices

We're obviously going to want to manage the list of choices on each element. This will require 3 things:

  • the ability to delete a choice
  • the ability to add a choice
  • the ability to edit a choice

Adding a choice

Since choices are stored in the choices array on a CHOICE node, adding a choice is a case of instructing the Toolkit update the choices array. We'll add a + button to the header of the CHOICE template, and an X button to each choice:

[ACTION_CHOICE]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-choice" data-jtk-target="true">
{{message}}
<div class="jtk-choice-add"></div>
<r-each in="choices" key="id">
<div class="jtk-chatbot-choice-option"
data-jtk-source="true"
data-jtk-port-type="choice"
data-jtk-port="{{id}}">
{{label}}
<div class="jtk-edge-delete"></div>
</div>
</r-each>
</div>`
}

and some CSS for these buttons:

.jtk-choice-add {
position: absolute;
top: 0.5rem;
right: 0.5rem;
color: white;
cursor: pointer;
}

.jtk-choice-add::after {
content:'+'
}

.jtk-choice-delete {
color:white;
margin-left:auto;
cursor: pointer;
}

.jtk-choice-delete::after {
content:'x';
}

To wire these up, we add a listener for each of them to the modelEvents parameter we pass to the render call:

toolkit.render(canvas, {

...,
modelEvents:[
{
event:EVENT_TAP,
selector:".jtk-choice-add",
callback:(event, eventTarget, modelObject) => {
toolkit.addPort(modelObject.obj, { id:uuid(), label:"Choice"})
}
},
{
event:EVENT_TAP,
selector:".jtk-choice-delete",
callback:(event, eventTarget, modelObject) => {
toolkit.removePort(modelObject.obj)
}
}
]

})

In order for addPort to know where to put the new port's data, we also had to update the way we create the Toolkit instance:

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

You can read about that concept in detail in our documentation.

Editing a choice

We're almost done now, but there's just one thing missing - we can't edit each choice. To do this, we'll first update our inspector so that it can handle being given either a node or a choice. We'll add a couple of constants:

const PROPERTY_LABEL = "label"
const CHOICE = "choice"

and we'll add a template for the choice type in the inspector:

[CHOICE_PORT]:`<div class="jtk-chatbot-inspector">
<input type="text" jtk-att="${PROPERTY_LABEL}" jtk-focus placeholder="enter label..."/>
</div>`

Notice the jtk-focus attribute on that template? It instructs the Toolkit to set it as the focus when the inspection event starts.

We'll update our template resolver so it can pick the appropriate template. If the incoming object is a node then we just do what we did before - look it up by its type. Otherwise we return the CHOICE template:

templateResolver:(obj) => {
if (isNode(obj)) {
return this.inspectors[obj.type]
} else {
return this.inspectors[CHOICE_PORT]
}
}

Now we want to trigger the choice inspector in two places - first, when a new choice is added, so we update the model event from before:

{
event:EVENT_TAP,
selector:".jtk-choice-add",
callback:(event, eventTarget, modelObject) => {
toolkit.setSelection(toolkit.addPort(modelObject.obj, { id:uuid(), label:"Choice"}))
}
}

and we also add a new model event to listen for a tap on a choice:

modelEvents:{

...,
{
event:EVENT_TAP,
selector:".jtk-chatbot-choice-option",
callback:(event, eventTarget, modelObject) => {
toolkit.setSelection(modelObject.obj)
}
}

}

The choice inspector in all its glory:


11:33am - undo/redo

The chatbot app is looking quite complete now. But we can make just one more change and still bring this in in record time - we'll use the Toolkit's ControlsComponent to add support for undo/redo and zooming. This is a straightforward process:

And this is what we get:


11:35am - final touches

When is a computer program finished? For most computer programmers the answer is, of course, never - we like to tinker. But for now I'm just going to do a last couple of things and call this "done".

Deleting nodes

A neat way to do this is to add a delete button to each node's template that, via CSS, is only visible when the node is selected. For example, here's the START node template now:

[START]:{
parent:SELECTABLE,
template:`<div class="jtk-chatbot-start">
<div class="jtk-delete"></div>
<div class="connect" data-jtk-source="true"/>
</div>`
}

The .jtk-delete button's visibility is managed via css:

.jtk-delete {
position:absolute;
...
display:none;
}

.jtk-delete::after {
content:'x';
}

.jtk-surface-selected-element .jtk-delete {
display:flex;
}
note

As you can see above, I have now declared the START node to have SELECTABLE as its parent, so that the user can click it, so that it can show a delete button. I did the same for the END node. The inspector will not choke if there is no template available to inspect a given node - it will just do nothing.

Palette styles

I reworked the styles for the palette to make it look neater:

info

One trick to keep in mind when styling items in a palette is that when they are being dragged to the canvas, their parent element is the document object. So if you style palette items with reference to some container, eg

.node-palette > div {
width:80px;
color:red;
}

you will lose that style when the element is dragged. Better to use a unique class name on the elements and then scope them via that in your CSS:

.my-palette-item {
width:80px;
color:red
}

The final styles for the palette in this app are:

.jtk-chatbot-palette-item {
margin:0.25rem 0;
color:white;
padding:0.5rem;
border-radius:5px;
width:90px;
text-align: center;
}

.jtk-chatbot-palette-item[data-type='choice'] {
background-color: var(--bg-choice);
}
.jtk-chatbot-palette-item[data-type='message'] {
background-color: var(--bg-message);
}
.jtk-chatbot-palette-item[data-type='input'] {
background-color: var(--bg-input);
}
.jtk-chatbot-palette-item[data-type='start'] {
background-color: var(--bg-start);
}
.jtk-chatbot-palette-item[data-type='end'] {
background-color: var(--bg-end);
}

11:47am - coffee

So that's 3 hours and 37 minutes to build a chatbot application from scratch. I hope this has given you a good insight into the power and flexibility of the Toolkit, and how it can assist you to quickly bring your own ideas to life. If you want to now see the app in action, you can find it here.

I need a coffee.


Will this work with Angular/React/Vue/Svelte?

As our friends at JointJS like to say - because they do not offer a deep integration with any frameworks - this app is compatible with Angular, Vue, React and Svelte. Being compatible is basically like you can put a bowl of soup on a table. The physics holds. You can take the HTML, CSS and JS from this demo and render it inside a component you rendered with some framework and it will work. But the jsPlumb Toolkit offers a level of integration well beyond mere compatibility - with jsPlumb you can use components from your framework of choice to render your nodes, allowing for real complexity and reactiveness. We will be writing a version of this application for all of our library integrations in the near future, and we'll document the process of porting from this vanilla JS app.


Get in touch!

The jsPlumb Toolkit is a very versatile library with a rich feature set. Making these starter applications is a chance for us to demonstrate and illustrate how it might look for your company. If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 2 min read
Simon Porritt

What's new?

In this release we've made a small tweak to the way we render custom overlays which supports automatically updating their orientation based upon the endpoints of their edge. Let's say we want to put this SVG at each end of our edges:



Since we want this at the start and at the end we'll write a factory to create a spec for a custom overlay:
function customOverlayFactory(location, color) {
return {
type:"Custom",
options:{
create:function(component) {
const d = document.createElement("div")
d.className="rotating-custom-overlay"
d.style.width = "16px"
d.style.height = "20px"
d.innerHTML = `
<svg width="100%" height="100%" viewBox="0 0 16 21">
<rect x="0" y="0" fill="${color}" width="10" height="21"/>
<rect x="10" y="7" fill="${color}" width="6" height="7"/>
</svg>`
return d
},
location:location
}
}
}

Then in the edges section of our view we'll add two of these:

view:{
edges:{
default:{
overlays:[
customOverlayFactory(0, "orangered"),
customOverlayFactory(1, "cadetblue"),
]
}
}
}

And this is the result:

CSS Rules

Part of this is achieved via CSS:

.rotating-custom-overlay[data-anchor-oy='1'] {
transform: translate(-2px, 6.5px) rotate(90deg);
}

.rotating-custom-overlay[data-anchor-oy='-1'] {
transform: translate(-8.5px, -18px) rotate(270deg);
}

.rotating-custom-overlay[data-anchor-ox='1'] {
transform: translate(0px, -11.5px) rotate(0deg);
}

.rotating-custom-overlay[data-anchor-ox='-1'] {
transform: translate(-15px, -8.5px) rotate(180deg);
}

What's next?

This adds a nice new capability to the Toolkit, and we're looking forward to seeing what you do with it. It also links nicely with a concept that we're preparing to introduce in 7.x - that of markers. These will be the natural evolution of overlays, and over the next few months we'll be sharing more information about what they are, how to use them, and why we think they're the future.


Read more

There's a full discussion of customer overlays in our documentation: https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/overlays#custom


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 10 min read
Simon Porritt

The jsPlumb Toolkit uses individual SVG elements to render edges and individual SVG/HTML elements to render nodes, which is a different approach to other libraries in this space. In this post we're going to discuss the reasons why we do this, give a little historical background, and also share our vision for the near future.

jsPlumb does not use a single SVG context element because we do not consider jsPlumb to be limited to being strictly a diagramming library. jsPlumb's approach to rendering - allowing you to use any HTML or SVG to render your nodes - means that it is capable of building a whole other class of applications that many other libraries in this space cannot, or at least not in a straightforward and maintainable way.

Where it started

The original Community edition of jsPlumb, before it was even known as the Community edition, was released in 2009, at a time when the browser landscape was markedly different from what it is now. Our first release used an HTML canvas for each edge, inspired by the majestic Yahoo Pipes application, beloved of Ajaxians everywhere. But we wanted to support IE6, because it still had a large market share, and IE6 had no canvas or SVG support. What it did have, though, was VML. VML elements behave in a page like canvas elements do: there is no notion of a top level container to group them all, like you can have with SVG.

Since our renderer needed to be written in such a way that the knowledge of canvas vs VML vs SVG was the very last consideration, we standardised on the approach of a separate element for each edge, positioned individually.

Where it's at

Element dragging

In this little snippet we've painted an outline on each edge's SVG element. As you drag the nodes around you can see the way the SVG elements are resized and repositioned:

Do you notice how the node you are dragging turns green and is always positioned above the other nodes in the display? That's because of this CSS rule:

.jtk-show-outline-demo.jtk-drag {
background-color: forestgreen;
z-index:11;
}

We render all of our nodes with the .jtk-show-outline-demo class. When a user starts to drag a node, the Toolkit assigns the .jtk-drag class, and removes it on drag stop. For us it means that with a simple CSS rule we can implement a useful piece of UX.

This is not easily done when rendering to a single SVG context element. If you render all of your nodes and edges inside a single SVG element, z index is defined only implicitly - by the index of each child element inside the parent. The further down in the SVG's list of child nodes an element is, the greater the implicit z index. The true power of UIs inside the browser comes from a combination of markup, javascript and CSS, and a single SVG context removes some of that power.

Rich UIs

The Toolkit's approach excels in the complexity of the elements you can use to render your nodes. The Toolkit allows you to use any HTML or SVG you wish to - you are not restricted to using SVG - and it will automatically respond to the computed size of your nodes. This greatly expands the range of applications that can be built with jsPlumb when compared with other libraries in this space. jsPlumb is not just a diagramming library. jsPlumb is an application building library.

HTML vs SVG rendering

Every now and then we come across misunderstandings such as this, regarding "HTML" node rendering:

Furthermore, relying on HTML for node rendering has consequences for the types of shapes that can be represented. Whereas rectangles are supported out-of-the-box, anything more complicated (e.g. polygons, ellipses or custom paths) is much more difficult to support than in SVG-based libraries.

which is not the case. Consider this HTML:

<div class="someClass">
<h1>Title</h1>
<input placeholder="enter a value here"/>
<svg width="300px" height="300px">
<rect x="50" y="50" width="100" height="70" fill="orangered" stroke-width="1" stroke="yellow"/>
<path d="M 10 10 L 30 200 L 250 150 L 300 300" stroke-width="2" stroke="green" fill="none"></path>
</svg>
</div>

...it has SVG embedded in it:

There is no such thing as HTML versus SVG for node rendering. Choosing HTML means you also get SVG. There is, though, a difference between choosing a single SVG rendering context vs one element per node/edge - more on this below.

Of course, given that SVG is a subset of HTML from the browser's perspective, if you want to render a node that's a pure shape it is easily done. Here's a hexagon:

<svg viewBox="0 0 173.2 200" width="80" height="80">
<path fill="forestgreen"
d="M86.6 0L173.2 50L173.2 150L86.6 200L0 150L0 50Z"></path>
</svg>

In the Toolkit we can use just this SVG on its own as our node template:

Notice also in this demonstration that we implemented the same z-index trick as we did above - the hexagon we are dragging has a different fill and is raised above the others:


.demo-hexagon.jtk-drag {
z-index:11;
}

.demo-hexagon.jtk-drag path {
fill:orangered;
}
Dynamic sizing

We can also very easily - in a declarative way - add support for dynamic sizing to these hexagons:

<svg viewBox="0 0 173.2 200" width="{{size}}" height="{{size}}">
<path fill="forestgreen"
d="M86.6 0L173.2 50L173.2 150L86.6 200L0 150L0 50Z"></path>
</svg>

Here, size is a field in our node data:

{
nodes:[
{ id:"1", left:50, top:50, size:80},
{ id:"2", left:450, top:150, size:120},
{ id:"3", left:250, top:200, size:150},
{ id:"4", left:380, top:350, size:40}
]
}

Notice how in these few examples just discussed there was no need for any programmatic manipulation of elements or of their sizing - we provided an HTML template, the browser figured out the layout using the markup in this template and the CSS in the page, and jsPlumb figured out the necessary placement of the edges afterwards.

HTML markup

Not being constrained to just using SVG, we can very easily create detailed UIs and have most of the heavy lifting in terms of layout handled by the browser. In this example we have a single node template:

<div class="jtk-show-outline-demo d-flex flex-column jtk-auto-size-node">
{{id}}
<textarea rows="{{rows}}">{{notes}}</textarea>
</div>

Which we use to render a few nodes that each have a rows member in their backing data:

{
"nodes":[
{ "id":"1", "left":50, "top":50, "rows":6 },
{ "id":"2", "left":300, "top":250, "rows":9 },
{ "id":"3", "left":500, "top":0, "rows":3 }
]
}

We see the size of the text area is different in each node - and we didn't have to do any special computation to achieve this, it's all just HTML and CSS.

note

You cannot drag the nodes above by their textarea elements. The Toolkit automatically excludes form controls from being used for dragging an element.

Did you press the resize button? We have an onclick handler attached to that button which invokes this function:

function resize() {
toolkit.getNodes().forEach(n => toolkit.updateNode(n, {rows:3}))
}

We set rows to 3 for every node and the Toolkit re-renders everything, repositioning the edges as required. That's all we had to do, because once again we're able to rely on HTML and CSS. In this case, also, the Toolkit is running a ResizeObserver to catch the resize event from the DOM, which is supported in all major browsers - for HTML elements. For SVG elements ResizeObserver support is patchy across browsers, and it isn't clear whether it will ever be supported for SVG shape elements such as rect, circle etc.

You'll have to resize the page if you want to resize again. Once you've resized, there's no going back.

Library integrations

One area where you can really benefit from the Toolkit's approach to rendering is when you use a library such as React, Angular, Vue or Svelte. The Toolkit's integration for each of these libraries lets you render each node as a component, with all of the richness that provides. This is a reworking of the HTML textarea example above that uses the Toolkit's React integration:

We rendered each of the nodes here as a React functional component:

const NodeComponent = ({ctx}) => {

const { vertex, toolkit } = ctx;

function clear() {
toolkit.updateNode(vertex, {notes:""})
}

function update(e) {
toolkit.updateNode(vertex, {notes:e.target.value})
}

return <div>
{vertex.data.id}
<textarea rows={vertex.data.rows} value={vertex.data.notes} onChange={update}></textarea>
<button onClick={clear}>Clear</button>
</div>
}
note

You cannot drag the nodes in the above snippet by their textarea element - the Toolkit automatically excludes a range of input form fields from being able to instigate a drag.

Here we see an example of the complexity that is easily handled by the Toolkit - our NodeComponent encapsulates a specific set of business logic in a React functional component, and the configuration to make the Toolkit use this component could not be simpler:

const viewOptions = () => {
return {
nodes: {
default: {
jsx: (ctx) => {
return <NodeComponent ctx={ctx}/>
}
}
}
}
}

Achieving similar results in Angular, Vue or Svelte is just as easy. The Toolkit is integrated with these libraries, not merely compatible.

Where it's going

There are of course tradeoffs to using a single element per edge and vertex, the most notable being performance. For large datasets there's an overhead having all of those DOM elements around. But not every application regularly deals in very large datasets, and in practise we find that with our current arrangement performance only becomes a concern once the number of vertices is up into the hundreds, which is far more than many applications require.

We said at the start of this article that we do not consider jsPlumb to be limited to being just a diagramming library - it can be used to create applications on a whole other level than just diagrams. But we do acknowledge that using jsPlumb to build a diagram creator does have complications.

We see two separate, but related, areas for us to investigate and to develop jsPlumb.

Performance

We'll be keeping the current rendering approach as we firmly believe that there is a whole class of applications beyond just diagrams for which it is the logical choice, but we are keen to tune it wherever possible. To that end, in the next release - 7.x - we have overhauled our rendering pipeline entirely, stripping away the duplication of code that existed between the original Community edition and the Toolkit edition, and refactoring connection painting to be more performant. We do not yet have a release date for 7.x.

Diagramming support

  • A few versions ago we introduced the concept of shape libraries, which is a means for you to render SVG shapes of various types onto the canvas. At the time we also shipped a set of "flowchart" shapes, and we'll soon be releasing a set of BPMN symbols.

  • Another great new feature in our latest release is client side support for export to SVG/PNG/JPG, used in conjunction with a shape library.

  • We're looking at releasing a single SVG renderer. This would mean that all edges and nodes would have to be SVG, limiting it's capabilities a little, but for diagramming applications a single SVG can suffice.


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 3 min read
Simon Porritt

What's new?

Export to SVG, PNG and JPG

This is an exciting new capability that the Toolkit offers. When using a Shape library to render the nodes in your UI, you can now export the Surface canvas to either SVG, PNG or JPG.

We've added support for this to our Flowchart builder starter app:

When you click one of these you'll see a preview window, from which you can download the export:

The Toolkit offers a lightweight UI to assist with the export, as shown above, which is easily customised via CSS, and also a lower-level programmatic API if you want to do something more bespoke.

Read more about SVG, PNG and JPG export in the Toolkit documentation.

Also checkout the the Angular integration documentation for a discussion of some useful methods related to this that we added to the Toolkit's angular service.

Miscellaneous updates

  • The Angular service was augmented with new methods to support exporting to SVG/PNG/JPG - see the Angular integration documentation for details.

  • The ShapeLibrary component now supports rendering labels to shapes as part of their SVG element. Previously a user was required to separately manage labels, but the shape library can now handle that for you. For documentation on this, see the shape libraries documentation

  • A new flag useHTMLElement is supported on Label overlays. Setting to false will result in the Toolkit using an SVG text element for the label overlay, rather than an HTML element.

  • A new flag useHTMLLabel was added to edge definitions, which you can use to override the new default behaviour of using an SVG text element for the default label.

Breaking changes

  • The ShapeLibraryPalette no longer registers a jtk-shape tag on the associated Surface. Instead, the shape library needs to be passed in the render options to the Surface. See the shape libraries documentation for details.

  • When you specify a label in an edge type, the Toolkit now creates an SVG text element for it, rather than an HTML element.

  • By default, hover events are now switched off in the UI layer. We expect this change to affect almost nobody at all. For a full discussion of the reasoning behind this, see the documentation on rendering at https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/rendering.

Bugfixes

  • Fixed an issue with the lasso plugin that would cause it to fail on devices with touch + pointer input methods.
  • Fixed an issue whereby a cssClass defined on an edge mapping would not be applied to a newly created edge.

Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 2 min read
Simon Porritt

Release 6.5.0 of the Toolkit is now available.

What's new?

We're pleased to announce the availability of a new connector type in 6.5.0 - Segmented. This connector consists of a set of straight line segments, and can be smoothed to a set of bezier curves. We've also shipped a path editor for this new connector type.

Segmented Connectors

This connector type can be useful for a number of different applications, such as workflow designers, flowchart builders, drawing applications - the list is endless. With the Segmented path editor the user can very easily split/discard segments and drag key points around the canvas.

The connector also supports a "smooth" mode, where the individual line segments are replaced by a set of Bezier splines:

This is how the path looks when the editor has been closed:

To see these connectors in action, take a look at the Segmented connectors demonstration.

For documentation, check out https://docs.jsplumbtoolkit.com/toolkit/6.x/lib/connectors#segmented

Miscellaneous updates

  • Orthogonal connector editor automatically trims any supplied geometry after load, to ensure the internal geometry is in a format that the editor can manage.

  • The jsplumbtoolkit-connector-editors.css stylesheet was refactored slightly, to introduce common base classes for edge handles, and to use CSS variables.

  • The controls component now detects the existence - or otherwise - of a Lasso plugin on the associated Surface, and does not draw the mode change buttons if the Lasso is not present.

Bugfixes

  • Fixed an issue whereby path tracing between groups was failing.

Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 7 min read
Simon Porritt

Release 6.2.5 of the Toolkit is now available. This release contains a bunch of useful new things, and sees an update to our most popular demonstrations to turn them into fully-fledged starter applications from which you can build your own apps quicker than ever.

What's new?

Many things. The full changelog is available here. In this post we're going to discuss all of the changes from 6.2.0 through to 6.2.5.

Shape Libraries

A shape library is an object that can assist in rendering SVG shapes into your nodes. One of our most popular demonstrations for a long time has been the Flowchart Builder, in which nodes are drawn using SVG as various shapes. Prior to release 6.2.0, though, these shapes were "hand-drawn" in the node templates and adding a new shape involved creating the SVG template and mapping it explicitly inside your app.

The Flowchart Builder starter app now uses a shape library to render the SVG in its nodes:

The Toolkit ships a default set of shapes that you can use - FLOWCHART_SHAPES, but it is straightforward to create your own shape sets. It would be easy, for example, to convert the Flowchart Builder into an Entity Relationship Diagram builder simply by swapping out the shapes used by the shape library. We may even make that the topic of a future blog post.

Shape Library Palettes

We've created a component called ShapeLibraryPalette that integrates tightly with a shape library to provide a palette of shapes that a user can drag on to your canvas. You can see this in action in the Flowchart Builder starter app linked above. The shape library palette takes the contents of some shape library (in the screenshot below, that's the FLOWCHART_SHAPES that ship with the Toolkit) and renders each shape, then instantiates a drop manager to allow users to drag new shapes onto a surface.

Our Angular, React, Vue2 and Vue3 integrations all contain support for shape libraries and shape library palettes. Because of the way Svelte operates we haven't added any specific support for these concepts to the Svelte integration - it's straightforward to hook into these things the way you would in a 'vanilla' app. But if you're a user of our Svelte integration and you've got any suggestions we'd be happy to hear from you.

Read the documentation about shape libraries and shape library palettes here

Inspectors

An inspector is an object that can assist you in implementing implementing form-based editors for objects in your dataset. For example, this is a node inspector from the Flowchart Builder starter app:

Inspectors integrate tightly with a Surface component, and have been written in a layered way that makes them agnostic of the specifics of the form they are managing: it is the user's responsibility to render the HTML they wish to use for a given inspector, so you are free to use anything you like, and the inspector will hook into your UI via a custom attribute you write in your code.

Inspectors can manage single objects, as shown above, and they can also manage multiple objects:

Support for inspectors was added to the Angular, Vue2, Vue and React integrations.

EdgeTypePickers

In the FlowchartBuilder's edge inspector a user can select an edge type by clicking on it:

We make use of an EdgeTypePicker to do this. This is a component that we created for the starter apps but which seemed like something that could be useful for licensees of the Toolkit, so we've pulled it into the @jsplumbtoolkit/browser-ui package now. There's a wrapper available for Angular, React, Vue2 and Vue3.

Read the documentation about inspectors and edge type pickers here.

Selection mode

The concept of 'mode' was added to selections: you can specify that a given selection may only contain one type of object (eg nodes, groups or edges), or that it can contain a mix. For more information see the documentation.

We use this concept in the Flowchart Builder starter app: a user can edit one or more nodes in an inspector, or one or more edges, but not a mixture of nodes or edges. Setting SelectionModes.isolated on the underlying Toolkit's selection means that the Toolkit will not support a selection of objects of different types.

Drag and drop updates

Several improvements were made to the SurfaceDropManager and DropManager classes:

  • Added the canvasDropFilter to SurfaceDropManager. The parent class DropManager already supported this but it was not exposed on the SurfaceDropManager prior to 6.2.0.

  • An optional dragSize parameter can be supplied to the drop manager, to set the size of nodes/groups that are being dragged.

  • An object being dragged onto a canvas will be automatically scaled so that it matches the current zoom of the canvas. This makes a big difference to the usability of the drag/drop, as users can see while dragging an element how it will fit into the canvas once it is dropped. This behaviour can be switched off if desired.

Surface updates

  • When dragging a new edge, the Surface element will now honour a type specifier returned from a beforeStartConnect interceptor. In previous versions the type would only be honoured once a new connection had been established. For more information see here.

  • Support for a modelEvents object has been added to the Surface's constructor. Instead of having to register model events after the fact you can now supply them with your render parameters.

Controls component

You may have seen this component in our various starter app/feature demonstrations:

We now ship this as a component that you can include in your apps. Wrappers for Angular, React, Vue2 and Vue3 are available.

Miscellaneous updates

  • Edge editors now snap anchor placeholders to the closest anchor when the user is relocating an anchor. Previously the placeholder would not be snapped until the use released the mouse button. The previous behaviour can be reinstated by setting snapToAnchors:false in the options you pass to the startEditing method.

  • Support for 'fragments' was added to custom tags in the default template engine. This is more of an internal feature, but could be leveraged by advanced users of the Toolkit. See documentation on templating for further information.

  • Label overlays now have a jtk-label-overlay class added to their element.

  • The default CSS stylesheet was updated with a visibility:hidden rule for .jtk-label-overlay:empty. For label overlays on which the label's value is currently an empty string, this will hide the overlay.

  • The CSS for the "controls" component that we ship along with each of the library integrations was copied out from jsplumbtoolkit-demo-support.css and into jsplumbtoolkit-controls.css, and the various selectors were updated to be more specific.

  • Several enhancements were made to the drawing tools plugin, specifically with regard to honouring a grid that is applied to the associated surface.

Bugfixes

  • Fixed issue with anchor placeholder not being centered correctly when editing an edge.
  • Fixed issue with Hierarchy layout where certain combinations of unattached roots and multiple parents could cause the layout to fail, due to a cache not being cleared.
  • Updates to the default template engine to ensure that elements written by custom tags are correctly updated when their vertex is updated.
  • Fixed issue in connector editor package where re-editing an edge that it was already editing would cause any extra overlays to be removed.
  • Fixed issue in bezier connector editor where control points were being dragged at twice the rate the mouse was moving.

Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 6 min read
Simon Porritt

This morning we've released version 6.0.0 of both the Toolkit and Community editions of jsPlumb.

What's new?

There is no functional change between version 6.0.0 and the last 5.x version, 5.13.7. What has changed, though, is the way that jsPlumb is packaged.

In 5.x we distributed both the Toolkit and Community editions as a set of packages, with the intention being that users could keep their code size down by omitting packages they did not need. In practice, though, all Toolkit users needed the core Toolkit packages - which were the largest - and all Community users needed the core Community packages, also the largest. The gain of distributing the code amongst several packages was negligible.

From a development perspective, the practice of spreading code around the various packages meant that the developer needed to know which package to go to in order to import something. For Toolkit users in particular, this could get tedious. Is the import in the Toolkit edition core? The Community edition core? Common? A renderer? etc.

So in 6.x we're distributing everything in a single Toolkit package and a single Community package, and users of each edition need only import one single package - either @jsplumbtoolkit/browser-ui or @jsplumb/browser-ui.

The packages contain a Common JS module, an ES6 module, and a UMD. For users who have a tree shaker incorporated in their build chain this setup will be good for their bundle sizes: exporting everything from a single package means the tree shaker can be very granular.

These packages are available now. For Community Edition users, @jsplumb/browser-ui version 6.0.0 is in the public NPM repository.

For Toolkit users who have an active subscription to new releases and to our NPM repository, @jsplumbtoolkit/browser-ui version 6.0.0 is in our NPM repository and can be downloaded from the download page.

We're still catching up on updating the docs and demonstrations - expect to see these changes over the next few days.

How to migrate?

The process of migrating to 6.x is straightforward.

Toolkit Edition

We've created a 6.x branch of the Flowchart Builder demonstration.

There were three things we needed to do:

  1. Update dependencies:

Previously:

"dependencies": {
"@jsplumbtoolkit/browser-ui-vanilla-2": "^5.12.0",
"@jsplumbtoolkit/drop": "^5.12.0",
"@jsplumbtoolkit/labels": "^5.12.0",
"@jsplumbtoolkit/print": "^5.12.0",
"@jsplumbtoolkit/dialogs": "^5.12.0",
"@jsplumbtoolkit/connector-editors-orthogonal": "^5.12.0",
"@jsplumbtoolkit/connector-orthogonal": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-drawing-tools": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-miniview": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-lasso": "^5.12.0",
"@jsplumbtoolkit/browser-ui-plugin-background": "^5.12.0"
}

Now:

"dependencies": {
"@jsplumbtoolkit/browser-ui": "^6.0.0"
}
  1. Update imports:

Previously:

import * as Dialogs from "@jsplumbtoolkit/dialogs"

import {
SurfaceRenderOptions,
Surface,
EVENT_TAP,
EVENT_CANVAS_CLICK,
EVENT_SURFACE_MODE_CHANGED,
SurfaceMode,
Connection,
BlankEndpoint,
ArrowOverlay,
LabelOverlay,
AnchorLocations,
DEFAULT,
ready,
newInstance
} from "@jsplumbtoolkit/browser-ui-vanilla-2"

import {
Edge,
Vertex,
ObjectInfo,
AbsoluteLayout,
uuid,
forEach,
EVENT_UNDOREDO_UPDATE,
UndoRedoUpdateParams,
ObjectData,
extend
} from "@jsplumbtoolkit/core"

import { EdgePathEditor } from "@jsplumbtoolkit/connector-editors"
import { createSurfaceManager } from "@jsplumbtoolkit/drop"
import { registerHandler } from "@jsplumbtoolkit/print"
import {DrawingToolsPlugin} from "@jsplumbtoolkit/browser-ui-plugin-drawing-tools"
import {MiniviewPlugin} from "@jsplumbtoolkit/browser-ui-plugin-miniview"
import {OrthogonalConnector} from "@jsplumbtoolkit/connector-orthogonal"

import * as ConnectorEditorOrthogonal from "@jsplumbtoolkit/connector-editors-orthogonal"
import {LassoPlugin} from "@jsplumbtoolkit/browser-ui-plugin-lasso"
import {CancelFunction} from "@jsplumbtoolkit/dialogs"

import {GeneratedGridBackground, GridTypes, BackgroundPlugin} from "@jsplumbtoolkit/browser-ui-plugin-background"

ConnectorEditorOrthogonal.initialize()

Now:

import {
Dialogs,
SurfaceRenderOptions,
Surface,
EVENT_TAP,
EVENT_CANVAS_CLICK,
EVENT_SURFACE_MODE_CHANGED,
SurfaceMode,
Connection,
BlankEndpoint,
ArrowOverlay,
LabelOverlay,
AnchorLocations,
DEFAULT,
ready,
newInstance,Edge,
Vertex,
ObjectInfo,
AbsoluteLayout,
uuid,
forEach,
EVENT_UNDOREDO_UPDATE,
UndoRedoUpdateParams,
ObjectData,
EdgePathEditor,
createSurfaceDropManager,
registerHandler,
DrawingToolsPlugin,
MiniviewPlugin,
OrthogonalConnector,
LassoPlugin,
CancelFunction,
GeneratedGridBackground,
GridTypes,
BackgroundPlugin
} from "@jsplumbtoolkit/browser-ui"

Note also that it is not necessary to invoke ConnectorEditorOrthogonal.initialize() in version 6.x of the Toolkit.

  1. Update the Dialogs constructor

Since everything is exported from a single package there were a few method name clashes - multiple packages, for example, exported a newInstance method. Most of those newInstance methods were just convenience methods and didn't do anything particularly useful. For example, this is how we rewrote the code that creates a Dialogs instance:

Previously:

const dialogs = Dialogs.newInstance({
...
})

Now:

const dialogs = new Dialogs ({
...
});

Sometime over the next few days we'll merge all of the 6.x demonstration branches into main.

Community Edition

All of the Community Edition demonstrations have been ported to use 6.0.0 now on the main branch. Here's what we did to migrate the Flowchart demonstration (this is for the Typescript demonstration):

  1. Update dependencies

Previously:

  "dependencies": {
"@jsplumb/browser-ui": "^5.0.0",
"@jsplumb/connector-flowchart": "^5.0.0"
},

Now:

  "dependencies": {
"@jsplumb/browser-ui": "^6.0.0"
},
  1. Update imports

Previous:

import {
ContainmentType,
EVENT_CLICK,
EVENT_CONNECTION_ABORT,
EVENT_CONNECTION_DRAG,
newInstance,
ready
} from "@jsplumb/browser-ui"

import {AnchorLocations, AnchorSpec,} from "@jsplumb/common"

import {
Connection,
ConnectionDetachedParams,
ConnectionMovedParams,
DotEndpoint,
EVENT_CONNECTION_DETACHED,
EVENT_CONNECTION_MOVED,
LabelOverlay
} from "@jsplumb/core"

import {FlowchartConnector} from "@jsplumb/connector-flowchart"

Now:

import {
ContainmentType,
EVENT_CLICK,
EVENT_CONNECTION_ABORT,
EVENT_CONNECTION_DRAG,
newInstance,
ready,
AnchorLocations,
AnchorSpec,
Connection,
ConnectionDetachedParams,
ConnectionMovedParams,
DotEndpoint,
EVENT_CONNECTION_DETACHED,
EVENT_CONNECTION_MOVED,
LabelOverlay,
FlowchartConnector
} from "@jsplumb/browser-ui"

What are the breaking changes?

We'll be updating the changelog in the documentation over the next few days, but for convenience here we include the changelog for each edition.

Toolkit breaking changes

  • Support for the original templates syntax (where variable interpolations are of the form ${...}) has been dropped, throughout the Vanilla toolkit and the dialogs. The code from the previous templates-2, dialogs-2 and browser-ui-vanilla-2 packages has been retained in the @jsplumbtoolkit/browser-ui package, and the code from templates, dialogs and browser-ui-vanilla has been dropped.

  • The Spring layout has been removed. Use ForceDirected instead.

In order to support the single @jsplumbtoolkit/browser-ui package, several factory methods were renamed/removed:

  • newInstance(surface) in previous @jsplumbtoolkit/browser-ui-anim removed. Use new SurfaceAnimator(surface) instead.

  • createManager in previous @jsplumbtoolkit/drop package is now exposed as createDropManager in @jsplumbtoolkit/browser-ui

  • createSurfaceManager in previous @jsplumbtoolkit/drop package is now exposed as createSurfaceDropManager in @jsplumbtoolkit/browser-ui

  • newInstance(surface:Surface, options?:ConnectorEditorOptions) from previous @jsplumbtoolkit/connector-editors method removed - use new EdgePathEditor(surface:Surface, options?:ConnectorEditorOptions) instead.

  • initialize() method from previous @jsplumbtoolkit/connector-editors-bezier is now exposed as initializeBezierConnectorEditors() on @jsplumbtoolkit/browser-ui.

  • initialize() method from previous @jsplumbtoolkit/connector-editors-orthogonal is now exposed as initializeOrthogonalConnectorEditors() on @jsplumbtoolkit/browser-ui.

  • newInstance(params:DialogsOptions) from the previous @jsplumbtoolkit/dialogs-2 package has been removed. To instantiate Toolkit dialogs now, use new Dialogs(params).

  • The HORIZONTAL and VERTICAL axis identifiers from the HierarchicalLayout have been moved into the enum HierarchicalLayoutOrientations.

  • The code from the related -drop package for each library integration (@jsplumbtoolkit/browser-ui-angular-drop etc) has now been pulled into the library integration's main package

Community breaking changes

  • newInstance method removed from @jsplumb/browser-ui-lists. Use new JsPlumbListManager(instance, params) instead.

  • BeforeStartDetachInterceptor renamed to BeforeStartConnectionDetachInterceptor

  • BeforeDetachInterceptor renamed to BeforeConnectionDetachInterceptor

  • BeforeDropInterceptor renamed to BeforeConnectionDropInterceptor


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 2 min read
Simon Porritt

Release 5.13.1 of the Toolkit is now available. This is a minor release consisting mainly of updates to the Hierarchy layout that came about through work on our series of World Cup visualizations.

Breaking changes

  • The Group class's getAllEdges method does not return edges connected to children of the group as of this release; this method behaves the same for Group as it does for Node now. It's not very likely that users of the API would be using this method.

Updates

  • We made some improvements to the way the Hierarchy layout places elements when invert is switched on.
  • We added a groupUnattachedRoots flag to the options for the Hierarchy layout. When a dataset contains one or more elements that have no child elements, the layout will ordinarily place these elements such that they do not intersect the area that the space child elements of the previous root take up. But when groupUnattachedRoots is set, these root nodes will be placed next to the previous root node. You can see this behaviour in the high level tournament view from our World Cup site:


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 3 min read
Simon Porritt

Today we released version 5.10.8 of the Toolkit edition of jsPlumb. In fact we're released a few versions in rapid succession as we've honed the API for a new piece of functionality: generated grid backgrounds.

We're excited to have added this functionality to the Toolkit - it provides a polish to applications using the Toolkit with an extremely easy to use API. The backgrounds themselves consist of a single SVG element with a repeated pattern, and so are very performant.

The full documentation for generated grid backgrounds can be found here.

But here's a quick example:

Grid background

This was generated with the following code:

import { 
AbsoluteLayout,
newInstance,
AnchorLocations,
BlankEndpoint,
StateMachineConnector,
GeneratedGridBackground,
BackgroundPlugin
} from "@jsplumbtoolkit/browser-ui"

const tk = newInstance()
const surface = tk.render(someElement, {

layout:{
type:AbsoluteLayout.type
},
defaults:{
anchor:AnchorLocations.Continuous,
endpoint:BlankEndpoint.type,
connector:StateMachineConnector.type
},
plugins:[
{
type:BackgroundPlugin.type,
options:{
type:GeneratedGridBackground.type,
minWidth:1500,
minHeight:1500
}
}

]
})

Changelog

The changelog for versions 5.10.6 through 5.10.8 is:

5.10.8

August 16th 2022

  • Internal refactoring of background plugin to expose type members that can be used when configuring backgrounds in an ES6/TS development environment, to avoid the code being omitted through tree shaking.

5.10.7

August 15th 2022

  • Added support for generated grid backgrounds.

5.10.6

August 12th 2022

  • Fixed a couple of issues with edge placement in nested nodes/groups in the underlying community edition
  • Fixed an issue with the tiled background layer in the background plugin that was preventing it from rendering.
  • Reinstated the zoomToBackground method on the Surface widget
  • Added supportLegacyConnectorData flag to OrthogonalConnectorOptions. This offers a means for people migrating from 2.x to load datasets where there is path data for Orthogonal connectors in the 2.x format.

Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.

· 2 min read
Simon Porritt

jsPlumb uses Docusaurus, a super handy static site generator, for various parts of our ecosystem, including our Documentation and also our recently released stand alone components product.

While developing the site for jsPlumb Components, we wanted to include a couple of pieces of information that would vary depending on whether we were running in development mode locally (ie docusaurus start) or building for production. We looked into the various options available, all based on the dotenv plugin for Webpack, but couldn't find what we were looking for: a solution that worked, with minimal manual intervention, in both development mode and when building for production.

So we ended up building our own plugin. This plugin reads values from a JSON file and makes them available to your site via the customFields map in the Docusaurus siteConfig.

note

When I say "Docusaurus" in this post I am talking about v2. I've not used v1.

Installation

npm i @jsplumb/docusaurus-plugin-env-loader-json

Configuration

You just have to add the plugin to the list of plugins in docusaurus.config.js:

plugins:[
"@jsplumb/docusaurus-plugin-env-loader-json"
],

and then create an env.json file in the Docusaurus project directory.

env.json

Your environment variables should be keyed under a section that identifies the environment you are targetting - "development" when using docusaurus start and "production" when running a build. The plugin reads the environment name from the environment variable NODE_ENV.

{
"production":{
"SERVER_URL":"https://some.server.com/anEndpoint"
},
"development":{
"SERVER_URL":"http://localhost:4200/anEndpoint"
}
}

Accessing values

You can access these values via the customFields section of the Docusaurus site config:

import useDocusaurusContext from '@docusaurus/useDocusaurusContext';

export function MyApp {

const {siteConfig} = useDocusaurusContext();
const serverUrl = siteConfig.customFields.SERVER_URL

...

}

Changing the configuration file name

If you want to specify some file other than env.json in your project's root, you can do so by setting the sourceFile option of the plugin:

plugins:[
[
"@jsplumb/docusaurus-plugin-env-loader-json",
{
"sourceFile":"path/to/aFile.json"
}
]
],

This path should be specified relative to the project root. No leading slash is required.


Get in touch!

If you'd like to discuss any of the ideas/concepts in this article we'd love to hear from you - drop us a line at hello@jsplumbtoolkit.com.

Not a user of the jsPlumb Toolkit but thinking of checking it out? Head over to https://jsplumbtoolkit.com/trial. It's a good time to get started with jsPlumb.