Skip to main content

Flowchart Builder

This application is designed to serve as a base from which you can build your own applications. It uses a number of features of the Toolkit, and we ship versions of it for all of the library integrations.

On this page we discuss the "vanilla" implementation. There are implementations available for each of our library integrations:

The source code for this application is available on Github at https://github.com/jsplumb/jsplumbtoolkit-applications.

Overview

The main features of this application are:

  • A drawing canvas which renders nodes as SVG shapes, and allows a user to resize nodes. Nodes are dragged on a grid, and resizing is constrained to the same grid.

  • A palette of SVG shapes which can be dragged onto the canvas.

  • Editable edge paths, labels and colors

  • Editable node labels, fill/outline color and text color

  • A miniview

  • A set of controls for managing zoom, undo, redo, etc.

Note that the code snippets on this page refer to constants in many places, which, for the sake of brevity, we have not included. You can find these in the source code on Github.

Throughout the code you will see references to a variable called anchorPositions. This is an array of anchor positions that dictate where on each node edges can be connected. It is declared at the start of the demonstration code:

const anchorPositions = [
{ x:0, y:0.5, ox:-1, oy:0, id:"left" },
{ x:1, y:0.5, ox:1, oy:0, id:"right" },
{ x:0.5, y:0, ox:0, oy:-1, id:"top" },
{ x:0.5, y:1, ox:0, oy:1, id:"bottom" }
]

Each anchor position has x, y, ox and oy values - see anchors for details of this. In addition, we set an id on each anchor, which is not part of the data model but rather is used for CSS purposes. This is discussed below.

Imports


"dependencies":{
"@jsplumbtoolkit/browser-ui":"^6.2.0"
}

Setup

At the heart of any application using the Toolkit is some instance of the Toolkit edition. In this application we create a Toolkit with these parameters:

const toolkit = newInstance({
// set the Toolkit's selection mode to 'isolated', meaning it can select a set of edges, or a set of nodes, but it
// cannot select a set of nodes and edges. In this demonstration we use an inspector that responds to events from the
// toolkit's selection, so setting this to `isolated` helps us ensure we dont try to inspect edges and nodes at the same
// time.
selectionMode:SelectionModes.isolated,
// This is the payload to set when a user begins to drag an edge - we return values for the
// edge's label, color and line style. If you wanted to implement a mechanism whereby you have
// some "current style" you could update this method to return some dynamically configured
// values.
beforeStartConnect:(node, edgeType) => {
return {
[PROPERTY_LABEL]:"",
[PROPERTY_COLOR]:DEFAULT_STROKE,
[PROPERTY_LINE_STYLE]:EDGE_TYPE_TARGET_ARROW
}
}
});

Rendering the canvas

This is the call to render the Toolkit instance. There are a number of things going on in here; we've documented this inline but also in subsequent sections we'll expand on various bits and pieces from here.

const canvasElement = document.querySelector(".jtk-demo-canvas")
const renderer = toolkit.render(canvasElement, {
//
// We use this to extract the text color from each object and set it on its DOM element in the template. Refer to
// the `color:{{#textColor}}` directive in the template's root element.
//
templateMacros:{
textColor:(data) => {
return data[PROPERTY_TEXT_COLOR] || DEFAULT_TEXT_COLOR
}
},
view: {
nodes: {
[DEFAULT]:{
//
// The node template writes out left/top/width/height properties from the underlying object in its style,
// as well as using the `textColor` macro to set color for the node's text.
//
// The `jtk-shape` tag injects an SVG shape whose form is determined by the `type` of each node. See the
// shape-libraries page for information about this.
//
// We iterate through `anchorPositions` and write out a connection source for each position found. Notice
// how we use the `id` from each anchor position in the element's css class: this is mapped in our stylesheet
// to position the element appropriately.
//
template:`
<div style="left:{{left}}px;top:{{top}}px;width:{{width}}px;height:{{height}}px;color:{{#textColor}}"
class="flowchart-object flowchart-{{type}}"
data-jtk-target="true"
data-jtk-target-port-type="target">

<jtk-shape/>

<span>{{text}}</span>

${anchorPositions.map(ap => `
<div class="jtk-connect jtk-connect-${ap.id}"
data-jtk-anchor-x="${ap.x}"
data-jtk-anchor-y="${ap.y}"
data-jtk-orientation-x="${ap.ox}"
data-jtk-orientation-y="${ap.oy}"
data-jtk-source="true"></div>`).join("\n")}

<div class="node-delete node-action delete"/>
</div>
</div>`,
// Declaring `anchorPositions` on the target port here will result in the Toolkit selecting the closest
// anchor position to the cursor location at the time that a new connection is established. This makes for
// an intuitive experience for users.
anchorPositions,
// any number of edges can be connected to this vertex
maxConnections: -1,
events: {
//
// Respond to `tap` events. Tap is preferable to EVENT_CLICK because click will fire even if
// a node has been dragged between mousedown and mouseup.
//
[EVENT_TAP]: (params) => {
// Cancel any edge edits when the user taps a node. `edgeEditor` is declared at the top level of
// the app code, and created once the surface has been rendered.
edgeEditor.stopEditing()
// If zero nodes currently selected, or the shift key wasn't pressed, make this node the only one in the selection.
if (toolkit.getSelection()._nodes.length < 1 || params.e.shiftKey !== true) {
toolkit.setSelection(params.obj)
} else {
// if multiple nodes already selected, or shift was pressed, add this node to the current selection.
toolkit.addToSelection(params.obj)
}
}
}
}
},
edges: {
[DEFAULT]: {
// Our edge uses a Blank endpoint and an Orthogonal connector.
endpoint:BlankEndpoint.type,
connector: {
type:OrthogonalConnector.type,
options:{
cornerRadius: 3,
alwaysRespectStubs:true
}
},
// we set a css class on the edge and also on its label
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
// This says 'extract `label` from the edge data and use it as the edge's label'.
label:"{{label}}",
events: {
[EVENT_CLICK]:(p) => {
// On edge click, select the edge (the inspector will update to
// show this edge), and start editing it.
toolkit.setSelection(p.edge)
edgeEditor.startEditing(p.edge, {
// Show a delete button.
deleteButton:true
})
}
}
}
}
},
// We declare a set of edge mappings to use (see the section `Edge types` below)
propertyMappings:{
edgeMappings:edgeMappings()
},
// Layout the nodes using an absolute layout. Our nodes contain `left` and `top` values to support this, and the Toolkit
// will keep those values updated as the user drags nodes around.
layout: {
type: AbsoluteLayout.type
},
// Snap everything to a grid. This will be used for element dragging as well as resizing, and also
// by the palette that allows users to drag new nodes on to the canvas.
grid:{
size:GRID_SIZE
},
events: {
// On whitespace click, clear selected node/edge and stop editing any edges.
[EVENT_CANVAS_CLICK]: (e) => {
toolkit.clearSelection()
edgeEditor.stopEditing()
}
},
// A selector identifying which parts of each node should not cause the element to be dragged.
// Typically here you'd list such things as buttons etc. We list the handles from the drawing plugin,
// and a selector identifying the node delete button.
dragOptions: {
filter: ".jtk-draw-handle, .node-action, .node-action i"
},
plugins:[
// Add a miniview plugin. `miniviewElement` is an element in our DOM.
{
type: MiniviewPlugin.type,
options: {
container: miniviewElement
}
},
// This plugin allows the user to resize elements. We specify `widthAttribute` and `heightAttribute` here because
// by default this plugin uses `w` and `h`. In version 7.x of the Toolkit we are updating all references to width and
// height to use `width` and `height` as this aligns better with the DOM's `getBoundingClientRect` method.
{
type:DrawingToolsPlugin.type,
options:{
widthAttribute:"width",
heightAttribute:"height"
}
},
// Select multiple elements with a lasso
{
type:LassoPlugin.type,
options: {
lassoInvert:true,
lassoEdges:true
}
},
// Draw a grid background. See `Background grid` below.
{
type:BackgroundPlugin.type,
options:GRID_BACKGROUND_OPTIONS
}
],
modelEvents:[
// catch the TAP event on the delete buttons inside nodes and remove the node from the model.
{
event:EVENT_TAP,
selector:".node-delete",
callback:(event, eventTarget, info) => {
toolkit.removeNode(info.obj)
}
}
]
})

Nodes

Rendering nodes

Nodes are rendered via the DEFAULT mapping in the nodes section of the view shown above. For users of a library integration this will be one key difference. For more information on views, see this page.

[DEFAULT]:{    
//
// The node template writes out left/top/width/height properties from the underlying object in its style,
// as well as using the `textColor` macro to set color for the node's text.
//
// The `jtk-shape` tag injects an SVG shape whose form is determined by the `type` of each node. See the
// shape-libraries page for information about this.
//
// We iterate through `anchorPositions` and write out a connection source for each position found. Notice
// how we use the `id` from each anchor position in the element's css class: this is mapped in our stylesheet
// to position the element appropriately.
//
template:`
<div style="left:{{left}}px;top:{{top}}px;width:{{width}}px;height:{{height}}px;color:{{#textColor}}"
class="flowchart-object flowchart-{{type}}"
data-jtk-target="true"
data-jtk-target-port-type="target">

<jtk-shape/>

<span>{{text}}</span>

${anchorPositions.map(ap => `
<div class="jtk-connect jtk-connect-${ap.id}"
data-jtk-anchor-x="${ap.x}"
data-jtk-anchor-y="${ap.y}"
data-jtk-orientation-x="${ap.ox}"
data-jtk-orientation-y="${ap.oy}"
data-jtk-source="true"></div>`).join("\n")}

<div class="node-delete node-action delete"/>
</div>
</div>`,
// allowed anchor positions for this node type
anchorPositions,
// allow any number of connections to this node type
maxConnections:-1,
events: {
//
// Respond to `tap` events. Tap is preferable to EVENT_CLICK because click will fire even if
// a node has been dragged between mousedown and mouseup.
//
[EVENT_TAP]: (params) => {
// Cancel any edge edits when the user taps a node. `edgeEditor` is declared at the top level of
// the app code, and created once the surface has been rendered.
edgeEditor.stopEditing()
// If zero nodes currently selected, or the shift key wasn't pressed, make this node the only one in the selection.
if (toolkit.getSelection()._nodes.length < 1 || params.e.shiftKey !== true) {
toolkit.setSelection(params.obj)
} else {
// if multiple nodes already selected, or shift was pressed, add this node to the current selection.
toolkit.addToSelection(params.obj)
}
}
}
}
}
note

The jtk-shape tag in the template above is what writes out the SVG for each node, and it is registered on the surface by the shape library palette that we attach in the app code. The jtk-shape tag uses the type from each node's data to find the appropriate SVG to write.

If you wanted to create a version of this application without the shape library palette (perhaps for a read only view?) then you would need to register the jtk-shape tag yourself. This is discussed in the shape library documentation.


Dragging new nodes

This application makes use of a shape library to render SVG into the nodes, and also to support a palette of SVG shapes from which the user can drag new nodes to the canvas.

The shape library is created at the root level of the app code:

import { FLOWCHART_SHAPES, ShapeLibraryImpl, ShapeLibraryPalette } from "@jsplumbtoolkit/browser-ui"

const shapeLibrary = new ShapeLibraryImpl([FLOWCHART_SHAPES]);

Once we've created the surface (via the render call shown above), we create the shape library palette:

new ShapeLibraryPalette ({
container:document.getElementById("myPaletteContainer"),
shapeLibrary,
surface:renderer,
dataGenerator:(el) => {
return {
textColor:DEFAULT_TEXT_COLOR,
outline:DEFAULT_OUTLINE,
fill:DEFAULT_FILL,
outlineWidth:DEFAULT_OUTLINE_WIDTH
}
}
})

The shape library palette does two things:

  • it renders the contents of the given shape library into the container element we specified
  • it registers a jtk-shape tag on the surface to which it is attached. This is only of use when you're using the Toolkit's default template renderer, of course.

When a user starts to drag a new shape from the palette, the dataGenerator is invoked, to get a data object for this new shape. The palette will adjust the zoom of the new shape to match the current zoom of the surface, and it will honour the surface's grid.

For more information about the ShapeLibraryPalette class, see this page.


Resizing nodes

Node resize is supported via the inclusion of the drawing tools plugin.

plugins:[
{
type:DrawingToolsPlugin.type,
options:{
widthAttribute:"width",
heightAttribute:"height"
}
}
]

In this application we set widthAttribute and heightAttribute because their defaults are w and h, and we are transitioning the Toolkit's UI code over to use width and height throughout the code, because those are the variable names used in the DOM. In 7.x the drawing tools plugin will be updated to use width and height by default.


Editing nodes

Nodes are edited via an inspector, declared in flowchart-inspector.js. This is a class that extends VanillaInspector:

const TMPL_NODE_INSPECTOR = "tmplNodeInspector"
const TMPL_EDGE_INSPECTOR = "tmplEdgeInspector"

export interface FlowchartInspectorOptions extends VanillaInspectorOptions { }

const inspectorTemplates = {
[TMPL_NODE_INSPECTOR] : `
<div class="jtk-inspector jtk-node-inspector">
<div class="jtk-inspector-section">
<div>Text</div>
<input type="text" jtk-att="text" jtk-focus/>
</div>

<div class="jtk-inspector-section">
<div>Fill</div>
<input type="color" jtk-att="${PROPERTY_FILL}"/>
</div>

<div class="jtk-inspector-section">
<div>Color</div>
<input type="color" jtk-att="${PROPERTY_TEXT_COLOR}"/>
</div>

<div class="jtk-inspector-section">
<div>Outline</div>
<input type="color" jtk-att="${PROPERTY_OUTLINE}"/>
</div>

</div>`,
[TMPL_EDGE_INSPECTOR] : `
<div class="jtk-inspector jtk-edge-inspector">
<div>Label</div>
<input type="text" jtk-att="${PROPERTY_LABEL}"/>
<div>Line style</div>
<jtk-line-style value="{{lineStyle}}" jtk-att="${PROPERTY_LINE_STYLE}"></jtk-line-style>
<div>Color</div>
<input type="color" jtk-att="${PROPERTY_COLOR}"/>
</div>`
}

/**
* Inspector for nodes/edges. We extend `VanillaInspector` here and provide a resolver to get an appropriate
* template based on whether the inspector is editing a node/nodes or an edge.
*/
export class FlowchartBuilderInspector extends VanillaInspector {

constructor(options:FlowchartInspectorOptions) {
super(Object.assign(options, {
templateResolver:(obj:Base) => {
if (isNode(obj)) {
return inspectorTemplates[TMPL_NODE_INSPECTOR]
} else if (isEdge(obj)) {
return inspectorTemplates[TMPL_EDGE_INSPECTOR]
}
}
}))

this.registerTag("jtk-line-style", createEdgeTypePickerTag(PROPERTY_LINE_STYLE, edgeMappings(), (v:string) => {
this.setValue(PROPERTY_LINE_STYLE, v)
}))

}
}

The key piece here is the templateResolver: VanillaInspector will call this method when it needs to render an inspector for some object (or set of objects).

In this application we have a single inspector for nodes and a single inspector for edges. But you could support multiple inspectors easily.

For notes on the edge inspector, see editing edges below.


Edges

Rendering edges

Edges are rendered via the DEFAULT mapping in the edges section of the view shown above. For more information on views, see this page.

edges: {
[DEFAULT]: {
// Our edge uses a Blank endpoint and an Orthogonal connector.
endpoint:BlankEndpoint.type,
connector: {
type:OrthogonalConnector.type,
options:{
cornerRadius: 3,
alwaysRespectStubs:true
}
},
// we set a css class on the edge and also on its label
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
// This says 'extract `label` from the edge data and use it as the edge's label'.
label:"{{label}}",
events: {
[EVENT_CLICK]:(p) => {
// On edge click, select the edge (the inspector will update to
// show this edge), and start editing it.
toolkit.setSelection(p.edge)
edgeEditor.startEditing(p.edge, {
// Show a delete button.
deleteButton:true
})
}
}
}
}

One particular point to note here is the EVENT_CLICK handler:

[EVENT_CLICK]:(p) => {
this.toolkit.setSelection(p.edge)
this.edgeEditor.startEditing(p.edge, {
deleteButton:true
})
}

This method does two things:

  • it sets the clicked edge to be the currently selected object in the Toolkit, which the attached inspector will detect, and then display an editor for the edge
  • it calls startEditing on the edge path editor, requesting a deleteButton be attached. Prior to version 6.3.0 you will want to pass anchorPositions in here, as the path editor did not automatically discover them. But from 6.3.0 onwards the path editor will automatically discover the anchor positions.

Edge overlays

The default edge type mapping declares a label:

label:"{{label}}"

This instructs the Toolkit to extract label from each edge's backing data and use it to display a label overlay (at location 0.5). The other overlays you see in this app are achieved via the use of edge property mappings to render different types of edges.

We support 5 types - a plain edge, a dashed edge, an edge with an arrow at the source, an edge with an arrow at the target, and an edge with an arrow at both ends.

These types are declared in the edge-mappings.ts file in the source:

export default function edgeMappings(arrowWidth, arrowLength) {

arrowWidth = arrowWidth || ARROW_WIDTH
arrowLength = arrowLength || ARROW_LENGTH

return [
{
property:PROPERTY_LINE_STYLE,
mappings:{
[EDGE_TYPE_SOURCE_ARROW]:{
overlays:[ { type:ArrowOverlay.type, options:{location:0, direction:-1, width:arrowWidth, Length:arrowLength} } ]
},
[EDGE_TYPE_TARGET_ARROW]:{
overlays:[ { type:ArrowOverlay.type, options:{location:1, width:arrowWidth, length:arrowLength} } ]
},
[EDGE_TYPE_BOTH_ARROWS]:{
overlays:[ {
type:ArrowOverlay.type,
options:{
location:1,
width:arrowWidth,
length:arrowLength
}
}, {
type:ArrowOverlay.type,
options:{
location:0,
direction:-1,
width:arrowWidth,
length:arrowLength
}
} ]
},
[EDGE_TYPE_PLAIN]:{},
[EDGE_TYPE_DASHED]:{
cssClass:CLASS_DASHED_EDGE
}
}
}
]

}

and are set on the surface in the render call shown above.

Default edge type

The beforeStartConnect method that we pass in to the Toolkit's constructor is the means we use to set the default payload for a new edge dragged by the user:

// This is the payload to set when a user begins to drag an edge - we return values for the
// edge's label, color and line style. If you wanted to implement a mechanism whereby you have
// some "current style" you could update this method to return some dynamically configured
// values.
beforeStartConnect:(node, edgeType) => {
return {
[PROPERTY_LABEL]:"",
[PROPERTY_COLOR]:DEFAULT_STROKE,
[PROPERTY_LINE_STYLE]:EDGE_TYPE_TARGET_ARROW
}
}

Editing edges

Edge label, color and "line style" are also edited via the FlowchartInspector: it contains a template for both nodes and for edges, and picks the appropriate one when something is selected.

Edge label and color are both set/changed by making use of the simple edge styles concept, in which there is a basic set of properties that the Toolkit will use to automatically set certain parts of the appearance of an edge.

The "line style" of an edge in this demonstration is a reference to what, if any, overlays the edge has, and whether it is a dashed line or not. As mentioned in the edge rendering section above, this is accomplished by edge property mappings.

In the edge inspector of this demonstration you will see a Line style section:

We use the EdgeTypePicker tag, discussed here to achieve this. The code that does this - and which uses the tag in the edge inspector template - is all in flowchart-inspector.js.


Editing edge paths

Paths can be edited in this application, via an EdgePathEditor. By default (since 6.2.11) this is created in active mode:

edgeEditor = new EdgePathEditor(renderer, {activeMode: true})

..but we leave the activeMode setter in the code to allow for versions prior to 6.2.11.

The call shown above creates an edge path editor but does not activate it. The path editor is activated in the click handler for edges:

edgeEditor.startEditing(p.edge, {
// Show a delete button.
deleteButton:true
})

Background grid

The application renders a background grid via the background plugin. The options we pass (shown above as GRID_BACKGROUND_OPTIONS) are:

{
dragOnGrid:true,
showGrid:true,
showBorder:false,
autoShrink:true,
minWidth:10000,
maxWidth:null,
minHeight:10000,
maxHeight:null,
showTickMarks:false,
type:GeneratedGridBackground.type
}

There is a discussion of each of these options on the background plugin page.


Miniview

We use a miniview in this starter app. It is declared as one of the plugins in the render call:


const miniviewElement = mainElement.querySelector(".miniview"),

...

plugins:[
{
type: MiniviewPlugin.type,
options: {
container: miniviewElement
}
},
]


Controls

Here we use another component that was created for our demonstrations but which now ships with the Toolkit, as we thought it might be useful for others:

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

const renderer = toolkit.render(....)
const controlsElement = document.querySelector(".jtk-controls-container")


CSS

Styles for the app itself are in app.css.

This app also imports a few of the CSS files that ship with the Toolkit:

<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css">
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-connector-editors.css">
<link rel="stylesheet" href="node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css">

jsplumbtoolkit.css provides basic 'structural' CSS, and it is recommended that you include this stylesheet in your cascade, at least when you first start developing with the Toolkit.

jsplumbtoolkit-connector-editors.css contains a mixture of structural and also theming styles, for use by the EdgePathEditor.

jsplumbtoolkit-controls.css contains styles for the controls component, which was created for our demonstrations but since 6.2.0 has been shipped with the Toolkit as it may be useful for others.