Flowchart Builder (React)

This is a port of the Flowchart Builder application that demonstrates the Toolkit's React integration. Version 1.11.0 of
the Toolkit contains several improvements to the React integration, the most notable being that it is now possible to
use React components to render nodes and groups.

For previous users of the React integration there are currently no breaking changes - your existing code will work with
version 1.11.0 - but be aware that the JsPlumbToolkitComponent is now deprecated and scheduled for removal in a future
release.

This page gives you an in-depth look at how the application is put together.

Font Awesome, jsplumbtoolkit-demo.css, and app.css are used for this demo and are not jsPlumb Toolkit requirements. jsplumbtoolkit-defaults.css is recommended for
all apps using the Toolkit, at least when you first start to build your app. This stylesheet contains sane defaults for the various widgets in the Toolkit.

JS

We use Webpack to create a bundle for the demonstration - JS dependencies are the dependencies section of package.json:

Demo Component

Implementation

This is the code for the demo component:

classDemoComponentextendsReact.Component{constructor(props){super(props);this.toolkit=jsPlumbToolkit.newInstance({nodeFactory:function(type,data,callback){Dialogs.show({id:"dlgText",title:"Enter "+type+" name:",onOK:(d)=>{data.text=d.text;// if the user entered a name...if(data.text){// and it was at least 2 charsif(data.text.length>=2){// set an id and continue.data.id=jsPlumbUtil.uuid();callback(data);}else// else advise the user.alert(type+" names must be at least 2 characters!");}// else...do not proceed.}});},beforeStartConnect:(node,edgeType)=>{// limit edges from start node to 1. if any other type of node, returnreturn(node.data.type==="start"&&node.getEdges().length>0)?false:{label:"..."};}});this.view={nodes:{"start":{component:StartComponent},"selectable":{events:{tap:(params)=>{this.toolkit.toggleSelection(params.node);}}},"question":{parent:"selectable",component:QuestionComponent},"action":{parent:"selectable",component:ActionComponent},"output":{parent:"selectable",component:OutputComponent}},// There are two edge types defined - 'yes' and 'no', sharing a common// parent.edges:{"default":{anchor:"AutoDefault",endpoint:"Blank",connector:["Flowchart",{cornerRadius:5}],paintStyle:{strokeWidth:2,stroke:"#f76258",outlineWidth:3,outlineStroke:"transparent"},// paint style for this edge type.hoverPaintStyle:{strokeWidth:2,stroke:"rgb(67,67,67)"},// hover paint style for this edge type.events:{"dblclick":(params)=>{Dialogs.show({id:"dlgConfirm",data:{msg:"Delete Edge"},onOK:()=>{this.toolkit.removeEdge(params.edge);}});}},overlays:[["Arrow",{location:1,width:10,length:10}],["Arrow",{location:0.3,width:10,length:10}]]},"connection":{parent:"default",overlays:[["Label",{label:"${label}",events:{click:(params)=>{this._editLabel(params.edge);}}}]]}},ports:{"start":{edgeType:"default"},"source":{maxConnections:-1,edgeType:"connection"},"target":{maxConnections:-1,isTarget:true,dropOptions:{hoverClass:"connection-drop"}}}}this.renderParams={// Layout the nodes using an absolute layoutlayout:{type:"Absolute"},events:{canvasClick:(e)=>{this.toolkit.clearSelection();},edgeAdded:(params)=>{if(params.addedByMouse){this._editLabel(params.edge,true);}}},lassoInvert:true,consumeRightClick:false,dragOptions:{filter:".jtk-draw-handle, .node-action, .node-action i"}}}render(){return<divstyle=><JsPlumbToolkitSurfaceComponentrenderParams={this.renderParams}toolkit={this.toolkit}view={this.view}ref={(c)=>this.surface=c.surface}/><ControlsComponentref={(c)=>this.controls=c}/><DatasetComponentref={(d)=>this.dataset=d}/><divclassName="miniview"/></div>}typeExtractor(el){returnel.getAttribute("data-node-type");}dataGenerator(type){return{w:120,h:80};}componentDidMount(){this.toolkit.load({url:"data/flowchart-1.json"});this.controls.initialize(this.surface);this.dataset.initialize(this.surface);newjsPlumbToolkit.DrawingTools({renderer:this.surface});ReactDOM.render(<DemoNodePalettesurface={this.surface}selector={"div"}typeExtractor={this.typeExtractor}container={nodePaletteElement}dataGenerator={this.dataGenerator}/>,document.querySelector(".node-palette"));ReactDOM.render(<JsPlumbToolkitMiniviewComponentsurface={this.surface}/>,document.querySelector(".miniview"))}_editLabel(edge,deleteOnCancel){Dialogs.show({id:"dlgText",data:{text:edge.data.label||""},onOK:(data)=>{this.toolkit.updateEdge(edge,{label:data.text||""});},onCancel:()=>{if(deleteOnCancel){this.toolkit.removeEdge(edge);}}});}}

This component has a few different responsibilities:

1. Creating an instance of the Toolkit

this.toolkit=jsPlumbToolkit.newInstance({nodeFactory:function(type,data,callback){Dialogs.show({id:"dlgText",title:"Enter "+type+" name:",onOK:(d)=>{data.text=d.text;// if the user entered a name...if(data.text){// and it was at least 2 charsif(data.text.length>=2){// set an id and continue.data.id=jsPlumbUtil.uuid();callback(data);}else// else advise the user.alert(type+" names must be at least 2 characters!");}// else...do not proceed.}});},beforeStartConnect:(node,edgeType)=>{// limit edges from start node to 1. if any other type of node, returnreturn(node.data.type==="start"&&node.getEdges().length>0)?false:{label:"..."};}});

We provide a nodeFactory, which is a function that jsPlumb calls when a new node is dropped onto the canvas. The node factory
is given the type of the node and any initial data (that was created via the dataGenerator plugged in to the drag/drop mechanism),
and a callback function that should be called with the given data if the factory wishes to proceed. In our implementation we popup a dialog
prompting the user for a node name, and if that name is two or more characters in length, we generate a random ID and hit the callback. If the
name is less than two characters in length we do not proceed.

We also provide a beforeStartConnect function, which returns an object. This object is used as the initial data for the edge that is
being drawn.

2. Rendering a Surface component, a node palette, a miniview, and a controls component

The Surface and controls components are rendered in the demo component's render method:

JsPlumbToolkitSurfaceComponent is discussed here. Note that in this
demo we use ref={ (c) => this.surface = c.surface } on its declaration in order to get
a reference to the component - we have to pass it in to various others.

ControlsComponent is a small helper component created to offer buttons for zoom,
lasso mode, and undo/redo. It isn't part of the Toolkit's React integration, but it is
production ready and can be used if you want to use it.

The miniview and palette components are created inside componentDidMount():

We do this because we know componentDidMount() won't be called until the Surface
component has been created, and we need the Surface component for the miniview and node
palette constructors, and also to initialize our controls.

3. Initialise the drawing tools

This occurs in componentDidMount(), because, again, we need a reference to the Surface widget
before we can initialise the drawing tools:

We pass in true for the second argument here, meaning the edge is new. _editLabel will
discard an edge if the user presses cancel on the dialog and this flag was set to true.

Node Components

There are four components used to render nodes - one each for the node types of Question, Action and Output, and one
for the Start node. The Output, Action and Question node components all extend the BaseEditableNodeComponent, which itself extends the
Toolkit's BaseNodeComponent. You must ensure your node components extend BaseNodeComponent.

The Start node consists of an ellipse with a text label centered inside of it. Note that all references to the node data that is being rendered are prefixed
with obj.. For instance, the first line in the template here is:

<divstyle=className="flowchart-objectflowchart-start">

As with the default templating mechanism (and in contrast to the Toolkit's Angular 1.x integration), we expect one
root element per template.

The jtk-source element declares that this node is an edge source, of type start (the port-type attribute
specifies this). The filter attribute instructs the Toolkit to enable drag only from some element that is not a
child of an svg element, but then filter-negate is true: the result is that dragging will begin only from a
descendant of the svg element. What this means visually is that the user will not be able to start a drag from the
whitespace surrounding the ellipse.

Resizing/Dragging Nodes

To resize or drag a node first you must either click on it, or use the lasso (described below) to select
it. A selected node looks like this:

The dotted line and drag handles that are added to a selected Node are put there by the Tookit's drawing tools. It
listens to the Toolkit's select/deselect events and
decorates UI elements accordingly. These tools are discussed in detail on this page.

The drawing tools are initialized with this line of code (inside the componentDidMount() method of the demo component):

newjsPlumbToolkit.DrawingTools({renderer:this.surface});

You pass them the instance of the Surface widget you're working with.

Dragging

Nodes can be dragged only by the square in the center of the node. This is achieved by setting a filter on the
dragOptions parameter in the renderParams:

dragOptions:{handle:".jtk-draw-drag"}

jtk-draw-drag is the classname of the square that the drawing tools place in the center of a selected node.

Resizing

Resizing is handled automatically by the drawing tools. By default, these tools will change the w, h, left and top values in a node's data, but this can be changed.

When a node's data is updated the drawing tools call the appropriate update method on the underlying Toolkit. The changes
will be reflected immediately in the DOM.

Selecting Nodes

Left Click

Nodes can be selected with a left-click (or tap on a touch device; tap is a better event to choose in general because the
Toolkit abstracts out the difference between mouse devices and touch devices and presents click events as tap events
on non touch devices). This is configured in the view parameter to the render call. In this application,
Nodes of type selectable have the capability enabled with this code:

The tap event (discussed here) is preferable to click, as it ensures the application responds only to true clicks on devices with a mouse, and also avoids the delay that some vendors introduce to a click event on touch devices.

Lasso Selection

Lasso selection is enabled by default on the Surface widget. To activate the lasso, click the pencil icon in the toolbar:

This icon (and the others in the toolbar) are rendered by the jsplumb-controls component, which is something we created for the
purposes of the Angular demonstrations and was originally maintained in each demonstration. For the purposes of maintainability we
moved it into the jsPlumbToolkitModule - the core Toolkit/Angular integration - so it is available to you once you import that module, but
remember its just one example of how to provide that functionality, and it makes a few assumptions. Read more about the controls
component angular-integration#controls.

Lasso Operation

The lasso works in two ways: when you drag from left to right, any node that intersects your lasso will be selected. When you drag from right to left, only nodes that are enclosed by your
lasso will be selected.

Exiting Select Mode

The Surface widget automatically exits select mode once the user has selected something. The jsplumb-controls component also
listen to clicks on the whitespace in the widget (in the flowchart component) and when one is detected, it clears the
current selection in the underlying Toolkit:

The syntax highlighter binds to the Toolkit's dataUpdated method, which is fired whenever any change is made to the data model. The implementation
of this method is not in the scope of this document, but you can find the code in the licensed or evaluation download.