Flowchart Builder (React)
This app is written using Hooks and is designed to work with React 17 or 18, but we import React 18. The code is very similar to the code in the vanilla flowchart builder, but it's just arranged slightly differently. The biggest difference is in the node rendering - in the Toolkit's React integration we render each node as a component. We also use components from the Toolkit's react integration for the miniview and pan/zoom controls.

On this page we discuss the React implementation. There are implementations available for each of our library integrations, and also for vanilla JS:
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
(which is defined in and exported by FlowchartComponent
). This is an array of anchor positions that dictate where on each node edges can be connected:
export 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-react":"^6.2.0"
}
Setup
App.js
The top level component wraps FlowchartComponent
:
import './App.css';
import FlowchartComponent from './FlowchartComponent'
function App() {
return (
<div className="App">
<FlowchartComponent/>
</div>
);
}
export default App;
FlowchartComponent
This is where the bulk of the code resides. The initial setup inside the component is as follows:
export default function FlowchartComponent() {
const shapeLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
const pathEditor = useRef(null)
const surfaceComponent = useRef(null)
const miniviewContainer = useRef(null)
const controlsContainer = useRef(null)
const paletteContainer = useRef(null)
const inspectorContainer = useRef(null)
/**
* Generator for data for nodes dragged from palette.
* @param el
*/
const dataGenerator = (el) => {
return {
fill:DEFAULT_FILL,
outline:DEFAULT_STROKE,
textColor:DEFAULT_TEXT_COLOR
}
}
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
}
}
})
initializeOrthogonalConnectorEditors()
...
}
Points to note:
- we create a shape library which we will use to render SVG inside of our nodes, and also to provide a palette of nodes that can be dragged onto the canvas.
- we assign a number of refs for things that we want to create in our
useEffect
(discussed below) dataGenerator
is the method used to get an initial dataset when the user is dragging new nodes onto the canvas.- We create an instance of the Toolkit and provide two arguments - what selection mode to use, and a method to get an initial payload when the user drags a new edge:
{
// 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 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
}
}
}
- We make a call to
initializeOrthogonalConnectorEditors()
. This is just an insurance policy against tree shaking. Because of the way in which connector editors are instantiated in 6.x (ie. by name), in some setups a tree shaker will omit them from the bundle. In 7.x this will not be an issue.
Rendering the canvas
The template for the flowchart component is as follows:
<div style={{width:"100%",height:"100%",display:"flex"}}>
<div className="jtk-demo-canvas">
<JsPlumbToolkitSurfaceComponent renderParams={renderParams} toolkit={toolkit} view={view} ref={ surfaceComponent }/>
<div className="controls" ref={ controlsContainer }/>
<div className="miniview" ref={ miniviewContainer }/>
</div>
<div className="jtk-demo-rhs">
<div className="node-palette sidebar" ref={paletteContainer}></div>
<div ref={inspectorContainer}/>
</div>
</div>
The canvas is rendered via the JsPlumbToolkitSurfaceComponent
component, which ships in the Toolkit's React integration. We pass in a view
and renderParams
(which are defined in this class - see below), the Toolkit instance we just created, and a ref
to use so that we can access the underlying surface in our useEffect
method.
View params
const view = {
nodes: {
[DEFAULT]: {
jsx: (ctx) => {
return <NodeComponent ctx={ctx} shapeLibrary={shapeLibrary}/>
},
// allowed anchor positions for this node type
anchorPositions,
// allow any number of connections to this node type
maxConnections:-1,
events: {
[EVENT_TAP]: (params) => {
pathEditor.current.stopEditing()
// if zero nodes currently selected, or the shift key wasnt 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)
}
}
}
}
},
// There are two edge types defined - 'yes' and 'no', sharing a common
// parent.
edges: {
[DEFAULT]: {
endpoint: BlankEndpoint.type,
connector: {
type: OrthogonalConnector.type,
options: {
cornerRadius: 5
}
},
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
label:"{{label}}",
outlineWidth:10,
events: {
[EVENT_DBL_CLICK]: (params) => {
toolkit.removeEdge(params.edge)
},
[EVENT_CLICK]: (params) => {
toolkit.setSelection(params.edge)
pathEditor.current.startEditing(params.edge, {
deleteButton:true
})
}
}
}
}
}
This view is almost identical to the view in the render call for the vanilla app. The key difference is that in the React integration nodes are rendered by React components.
Render params
const renderParams = {
layout:{
type:AbsoluteLayout.type
},
grid:{
size:GRID_SIZE
},
events: {
[EVENT_CANVAS_CLICK]: (e) => {
toolkit.clearSelection()
pathEditor.current.stopEditing()
}
},
propertyMappings:{
edgeMappings:edgeMappings()
},
consumeRightClick: false,
dragOptions: {
filter: ".jtk-draw-handle, .node-action, .node-action i"
},
plugins:[
{
type:DrawingToolsPlugin.type,
options:{
widthAttribute:"width",
heightAttribute:"height"
}
},
{
type:LassoPlugin.type,
options: {
lassoInvert:true,
lassoEdges:true
}
},
{
type:BackgroundPlugin.type,
options:GRID_BACKGROUND_OPTIONS
}
],
zoomToFit:true
}
The render params are almost identical to the render params for the vanilla app, with one notable exception being that in the React app we use a JsPlumbToolkitMiniview
(instantiated in the useEffect
), whereas in the vanilla app we configure a miniview as a plugin. Another difference is that we do not have a modelEvents
section in the render params for the React app, because we write that logic directly into the node component.
useEffect
We declare a useEffect
method to complete our setup:
useEffect(() => {
pathEditor.current = new EdgePathEditor(surfaceComponent.current.surface, {activeMode:true})
// controls component. needs to be done here as it needs a reference to the surface.
const c = createRoot(controlsContainer.current)
c.render(<ControlsComponent surface={surfaceComponent.current.surface}/>)
// a miniview.
const m = createRoot(miniviewContainer.current)
m.render(
<JsPlumbToolkitMiniviewComponent surface={surfaceComponent.current.surface}/>
);
// palette from which to drag new shapes onto the canvas
const slp = createRoot(paletteContainer.current)
slp.render(<ShapeLibraryPaletteComponent surface={surfaceComponent.current.surface} shapeLibrary={shapeLibrary} container={paletteContainer.current} dataGenerator={dataGenerator}/>);
// node/edge inspector.
const ic = createRoot(inspectorContainer.current)
ic.render(<Inspector surface={surfaceComponent.current.surface} container={inspectorContainer.current} edgeMappings={edgeMappings()}/>)
// load an initial dataset
toolkit.load({url:"/copyright.json"})
}, [])
This method does several things:
- Instantiates and stores on a ref an edge path editor.
- Mounts a
ControlsComponent
(see below) - Mounts a
JsPlumbToolkitMiniviewComponent
(see Miniview) - Mounts a
ShapeLibraryPaletteComponent
(see dragging new nodes) - Mounts an inspector (see editing nodes and editing edges)
- Loads an initial dataset.
Nodes
Rendering Nodes
In the view above, we map a single node type DEFAULT
to a react component:
nodes: {
[DEFAULT]: {
jsx: (ctx) => {
return <NodeComponent ctx={ctx} shapeLibrary={shapeLibrary}/>
},
// allowed anchor positions for this node type
anchorPositions,
// allow any number of connections to this node type
maxConnections:-1,
events: {
[EVENT_TAP]: (params) => {
pathEditor.current.stopEditing()
// if zero nodes currently selected, or the shift key wasnt 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)
}
}
}
}
}
ctx
comes from the Toolkit's React integration. shapeLibrary
was created at the start of the flowchart component, and is what this node component uses to render the SVG shape.
The component's code looks like this:
import * as React from 'react';
import { ShapeComponent } from "@jsplumbtoolkit/browser-ui-react"
import { anchorPositions } from "./FlowchartComponent";
export default function NodeComponent({ctx, shapeLibrary}) {
const { vertex, toolkit } = ctx;
const data = vertex.data;
return <div style={{width:data.width + 'px',height:data.height + 'px',color:data.textColor}} className="flowchart-object" data-jtk-target="true" data-jtk-target-port-type="target">
<span>{data.text}</span>
<ShapeComponent obj={data} shapeLibrary={shapeLibrary}/>
{anchorPositions.map(ap => <div className={"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" data-jtk-port-type="source"></div>)}
<div className="node-delete node-action delete" onClick={() => toolkit.removeNode(vertex)}></div>
</div>
}
Points to note:
- We write out values for
width
,height
andcolor
into the root element's style. In this app, each node's backing data contains values for width and height, and we use a Drawing Tools plugin to allow the user to resize elements. - We use a
ShapeComponent
to write out the SVG for each node. See below. - We import
anchorPositions
from the main component and iterate through its values to write out a set of elements from which connections can be dragged - We use the
removeNode()
method from the Toolkit'sBaseNodeComponent
to remove a node from the dataset
Shape library
This application uses a shape library to support rendering SVG into node elements.
Configuration for this comes in two parts:
Instantiating a shape library
We do this in the constructor of the FlowchartComponent:
import { FLOWCHART_SHAPES } from "@jsplumbtoolkit/browser-ui"
export default function FlowchartComponent() {
const shapeLibrary = new ShapeLibraryImpl(FLOWCHART_SHAPES)
...
}
Rendering shapes
The shape library is injected into each node in the view:
const view = {
nodes: {
[DEFAULT]: {
jsx: (ctx) => {
return <NodeComponent ctx={ctx} shapeLibrary={shapeLibrary}/>
}
}
}
...
}
The node templates uses a ShapeComponent
, which ships in the Toolkit's React integration:
<ShapeComponent obj={data} shapeLibrary={shapeLibrary}/>
ShapeComponent
extracts the type
member from the given object data and then renders an appropriate piece of SVG.
Dragging new nodes
This application makes use of a ShapeLibraryPaletteComponent
to support dragging new nodes onto the canvas. It is mounted inside the FlowchartComponent's useEffect
:
const slp = createRoot(paletteContainer.current)
slp.render(<ShapeLibraryPaletteComponent surface={surfaceComponent.current.surface} shapeLibrary={shapeLibrary} container={paletteContainer.current} dataGenerator={dataGenerator}/>);
We pass in 4 values:
- surface The surface to attach to
- shapeLibrary The shape library we created, which has the SVG to render our various node types.
- container The DOM element to render into
- dataGenerator A method to get an initial payload for any element that is being dragged out of the palette:
/**
* Generator for data for nodes dragged from palette.
* @param el
*/
const dataGenerator = (el) => {
return {
fill:DEFAULT_FILL,
outline:DEFAULT_STROKE,
textColor:DEFAULT_TEXT_COLOR
}
}
Resizing nodes
Node resize is supported via the inclusion of the drawing tools plugin, in the renderParams
:
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 the InspectorComponent
, which is a React component that internally creates an Inspector. The code is as follows:
export default function InspectorComponent({surface, edgeMappings}) {
const container = useRef(null)
const [currentType, setCurrentType] = useState('')
const [inspector, setInspector] = useState(null)
useEffect(() => {
setInspector(new Inspector({
container:container.current,
surface,
renderEmptyContainer:() => setCurrentType(''),
refresh:(obj, cb) => {
setCurrentType(obj.objectType)
// next tick
setTimeout(cb)
}
}))
}, [])
return <div ref={container}>
{ currentType === Node.objectType &&
<div className="jtk-inspector jtk-node-inspector">
<div>Text</div>
<input type="text" jtk-att="text" jtk-focus="true"/>
<div>Fill</div>
<input type="color" jtk-att="fill"/>
<div>Color</div>
<input type="color" jtk-att="textColor"/>
<div>Outline</div>
<input type="color" jtk-att="outline"/>
</div>
}
{ currentType === Edge.objectType &&
<div className="jtk-inspector jtk-edge-inspector">
<div>Label</div>
<input type="text" jtk-att="label"/>
<div>Line style</div>
<EdgeTypePickerComponent edgeMappings={edgeMappings} propertyName="lineStyle" inspector={inspector}/>
<div>Color</div>
<input type="color" jtk-att="color"/>
</div>
}
</div>
}
The call to create a new inspector takes four arguments:
- container the DOM element the inspector should use to apply/extract values. Here it is the component's native element.
- surface The surface to attach the inspector to.
- renderEmptyContainer The inspector calls this method when nothing is selected. Here, we set the
currentType
member to an empty string, which will result in the template rendering an empty div element (see below). - refresh The inspector calls this method when it needs this component to render an appropriate UI for a newly selected object (or objects). This method passes in the selected object and a callback to invoke once the UI is ready. We set
currentType
to the object's type, and then we set a timeout which will invoke the callback after this tick is complete. When the timeout fires and the callback is invoked the UI will have been updated.
Edges
Rendering edges
Edges are rendered via the DEFAULT
mapping in the edges
section of the viewParams
shown above. For more information on views, see this page.
edges: {
[DEFAULT]: {
endpoint:BlankEndpoint.type,
connector: {
type:OrthogonalConnector.type,
options:{
cornerRadius: 3,
alwaysRespectStubs:true
}
},
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
label:"{{label}}",
events: {
[EVENT_CLICK]:(p) => {
toolkit.setSelection(p.edge)
pathEditor.startEditing(p.edge, {
deleteButton:true
})
}
}
}
}
One particular point to note here is the EVENT_CLICK
handler:
[EVENT_CLICK]:(p) => {
toolkit.setSelection(p.edge)
pathEditor.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 adeleteButton
be attached. Prior to version 6.3.0 you will want to passanchorPositions
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 params.
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:
pathEditor.current = 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:
pathEditor.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, via the JsPlumbToolkitMiniviewComponent
component that ships with the React integration.
This is mounted inside the useEffect
:
// a miniview.
const m = createRoot(miniviewContainer.current)
m.render(
<JsPlumbToolkitMiniviewComponent surface={surfaceComponent.current.surface}/>
);
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:
// controls component. needs to be done here as it needs a reference to the surface.
const c = createRoot(controlsContainer.current)
c.render(<ControlsComponent surface={surfaceComponent.current.surface}/>)
CSS
Styles for the app itself are in app.css
and index.css
.
styles.css
also imports a few of the CSS files that ship with the Toolkit:
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css";
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-connector-editors.css";
@import "../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.