DEMOS
DOCS
FEATURES
DOWNLOAD
PURCHASE
CONTACT
BLOG

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.

Flowchart Builder Demonstration

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

Babel Setup

This demonstration uses JSX files and ES2015. To transpile these to ES5, we use Babel.

These are the package.json entries we use:

"devDependencies":{

    ...

    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.22.0",

    ...
}

TOP


package.json

This is the full package.json for the demonstration:

{
  "name": "jsplumbtoolkit-react",
  "version": "*.*.*",
  "description": "Integration between jsPlumb Toolkit and React",
  "main": "index.js",
  "author": "jsPlumb <hello@jsplumbtoolkit.com> (https://jsplumbtoolkit.com)",
  "license": "Commercial",
  "scripts": {
    "build": "node ./node_modules/webpack/bin/webpack.js"
  },
  "dependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "font-awesome": "^4.7.0",
    "jsplumbtoolkit": "file:../../jsplumbtoolkit.tgz",
    "jsplumbtoolkit-react": "file:../../jsplumbtoolkit-react.tgz",
    "jsplumbtoolkit-undo-redo": "file:../../jsplumbtoolkit-undo-redo.tgz"
  },
  "devDependencies": {
    "babel-core": "^6.22.1",
    "babel-loader": "^6.2.10",
    "babel-preset-es2015": "^6.22.0",
    "babel-preset-react": "^6.22.0",
    "webpack": "^2.4.1",
    "webpack-dev-server": "^2.4.1"
  }
}

Note that we import the jsPlumb Toolkit and its React integration via local file references to their respective packages, which are included in licensed (and evaluation) downloads.

TOP


Webpack Setup

This is the config file we use to setup Webpack to bundle our demonstration:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: './src/index.jsx',
  output: { 
      path: path.join(__dirname, "dist"), 
      filename: 'bundle.js' 
    },
  module: {
    loaders: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        query: {
          presets: ['es2015', 'react']
        }
      }
    ]
  },
};

TOP


Page Setup

CSS
<link rel="stylesheet" href="node_modules/font-awesome/css/font-awesome.min.css">
<link rel="stylesheet" href="node_modules/jsplumbtoolkit/dist/css/jsplumbtoolkit-defaults.css">
<link rel="stylesheet" href="node_modules/jsplumbtoolkit/dist/css/syntax-highlighter.css">
<link rel="stylesheet" href="node_modules/jsplumbtoolkit/dist/css/jsplumbtoolkit-demo.css">

<link rel="stylesheet" href="app.css">

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:

"dependencies": {
    "react": "^16.0.0",
    "react-dom": "^16.0.0",
    "jsplumbtoolkit": "file:../../jsplumbtoolkit.tgz",
    "jsplumbtoolkit-react": "file:../../jsplumbtoolkit-react.tgz"
}

jsPlumb has been tested against versions 15.4.2, 15.5.0 and 16.0.0 of React.

TOP


Demo Component

The entry point to the demonstration is index.jsx. It consists of a DemoComponent, which is rendered as follows:

ReactDOM.render(<DemoComponent/>, document.querySelector(".jtk-demo-canvas"));

Implementation

This is the code for the demo component:

class DemoComponent extends React.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 chars
                            if (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, return
                return (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 layout
            layout: {
                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 <div style=>
                    <JsPlumbToolkitSurfaceComponent renderParams={this.renderParams} toolkit={this.toolkit} view={this.view} ref={ (c) => this.surface = c.surface }/>
                    <ControlsComponent ref={(c) => this.controls = c }/>
                    <DatasetComponent ref={(d) => this.dataset = d }/>
                    <div className="miniview"/>                        
                </div>
    }

    typeExtractor (el) { return el.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);
        new jsPlumbToolkit.DrawingTools({
            renderer: this.surface
        });

        ReactDOM.render(
            <DemoNodePalette surface={this.surface} selector={"div"} typeExtractor={this.typeExtractor} container={nodePaletteElement} dataGenerator={this.dataGenerator}/>
            , document.querySelector(".node-palette"));

        ReactDOM.render(
            <JsPlumbToolkitMiniviewComponent surface={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 chars
                    if (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, return
        return (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:

render() {
    return <div style=>
                <JsPlumbToolkitSurfaceComponent renderParams={this.renderParams} toolkit={this.toolkit} view={this.view} ref={ (c) => this.surface = c.surface }/>
                <ControlsComponent ref={(c) => this.controls = c }/>
                <div className="miniview"/>                        
            </div>
}

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():

componentDidMount() {
    this.toolkit.load({url:"data/flowchart-1.json"});
    this.controls.initialize(this.surface);
    new jsPlumbToolkit.DrawingTools({
        renderer: this.surface
    });

    ReactDOM.render(
        <DemoNodePalette surface={this.surface} selector={"div"} typeExtractor={this.typeExtractor} container={nodePaletteElement} dataGenerator={this.dataGenerator}/>
        , document.querySelector(".node-palette"));

    ReactDOM.render(
        <JsPlumbToolkitMiniviewComponent surface={this.surface}/>, document.querySelector(".miniview")
    )
}

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:

new jsPlumbToolkit.DrawingTools({
    renderer: this.surface
});
4. Load the initial dataset
componentDidMount() {
    this.toolkit.load({url:"data/flowchart-1.json"});
    ...
}
5. Edit the label of an edge

_editLabel is a method declared on the demo component class:

_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);
            }
        }
    });
}

It is called in two places. For existing edges, it is called when the user clicks the label. This is wired up in the view:

edges: {
    ...

    "connection":{
        parent:"default",
        overlays:[
            [ "Label", {
                label: "${label}",
                events:{
                    click:(params) => {
                        this._editLabel(params.edge);
                    }
                }
            }]
        ]
    }
}

For new edges, it is called when the component receives an edgeAdded event:

this.renderParams = {
    // Layout the nodes using an absolute layout
    layout: {
        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"
    }
}

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.

StartComponent
import React from 'react';
import { BaseNodeComponent } from"jsplumbtoolkit-react";

/**
 * Component used to render a start node.
 */
export class StartComponent extends BaseNodeComponent {

    constructor(props) {
        super(props)
    }

    render() {

        const obj = this.state;

        return <div style= className="flowchart-object flowchart-start">
            <div style=>
                <svg width={obj.w} height={obj.h}>
                    <ellipse cx={obj.w / 2} cy={obj.h / 2} rx={obj.w / 2} ry={obj.h / 2} className="outer"/>
                    <ellipse cx={obj.w / 2} cy={obj.h / 2} rx={(obj.w /2) - 10} ry={(obj.h/2) - 10} className="inner"/>
                    <text textAnchor="middle" x={obj.w / 2} y={ obj.h / 2 } dominantBaseline="central">{obj.text}</text>
                </svg>
            </div>
            <jtk-source port-type="start" filter=".outer" filter-negate="true"/>
        </div>
    }
}

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:

<div style= className="flowchart-object flowchart-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.

QuestionComponent

import React from 'react'; import { BaseEditableComponent } from"./base-component.jsx";

/** * Component used to render a Question node. */ export class QuestionComponent extends BaseEditableComponent {

constructor(props) {
    super(props);
}

render() {

    const obj = this.state;

    return <div style= className="flowchart-object flowchart-question">
        <div style=>
            <div className="node-edit node-action" onClick={this.edit.bind(this)}>
                <i className="fa fa-pencil-square-o"/>
            </div>
            <div className="node-delete node-action" onClick={this.remove.bind(this)}>
                <i className="fa fa-times"/>
            </div>
            <svg width={obj.w} height={obj.h}>
                <path d={'M' +  (obj.w/2) + ' 0 L ' + obj.w + ' ' +  (obj.h/2) + ' L ' + (obj.w/2) + ' ' + obj.h + '  L 0 ' + (obj.h/2) + '  Z'} className="outer"/>
                <path d={'M' +  (obj.w/2) + ' 10 L ' + (obj.w - 10) + ' ' +  (obj.h/2) + ' L ' + (obj.w/2) + ' ' + (obj.h - 10) + '  L 10 ' + (obj.h/2) + '  Z'} className="inner"/>
                <text textAnchor="middle" x={obj.w/2} y={obj.h/2} dominantBaseline="central">{obj.text}</text>
            </svg>
        </div>
        <jtk-source port-type="source" filter=".outer"/>
        <jtk-target port-type="target"/>
    </div>
}

}

The Question node draws a diamond, and declares itself to be an edge target and source.

ActionComponent
import React from 'react';
import { BaseEditableComponent } from"./base-component.jsx";

/**
 * Component used to render an action node.
 */
export class ActionComponent extends BaseEditableComponent {

    constructor(props) {
        super(props)
    }

    render() {

        const obj = this.state;

        return <div style= className="flowchart-object flowchart-action">
            <div style=>
                <div className="node-edit node-action" onClick={this.edit.bind(this)}>
                    <i className="fa fa-pencil-square-o"/>
                </div>
                <div className="node-delete node-action" onClick={this.remove.bind(this)}>
                    <i className="fa fa-times"/>
                </div>
                <svg width={obj.w} height={obj.h}>
                    <rect x={0} y={0} width={obj.w} height={obj.h} className="outer drag-start"/>
                    <rect x={10} y={10} width={obj.w-20} height={obj.h-20} className="inner"/>
                    <text textAnchor="middle" x={obj.w/2} y={obj.h/2} dominantBaseline="central">{obj.text}</text>
                </svg>
            </div>
            <jtk-target port-type="target"/>
            <jtk-source port-type="source" filter=".outer"/>
        </div>
    }
}

Once again we use the position and dimensions for the node's main container as well as its SVG elements. Action nodes are configured as both edge sources and targets.

OutputComponent
import React from 'react';
import { BaseEditableComponent } from"./base-component.jsx";

/**
 * Component used to render an output node.
 */
export class OutputComponent extends BaseEditableComponent {

    constructor(props) {
        super(props)
    }

    render() {

        const obj = this.state;

        return <div style= className="flowchart-object flowchart-output">
            <div style=>
                <div className="node-edit node-action" onClick={this.edit.bind(this)}>
                    <i className="fa fa-pencil-square-o"/>
                </div>
                <div className="node-delete node-action" onClick={this.remove.bind(this)}>
                    <i className="fa fa-times"/>
                </div>
                <svg width={obj.w} height={obj.h}>
                    <rect x={0} y={0} width={obj.w} height={obj.h}/>
                    <text textAnchor="middle" x={obj.w/2} y={obj.h/2} dominantBaseline="central">{obj.text}</text>
                </svg>
            </div>
            <jtk-target port-type="target"/>
        </div>
    }
}

The Output node is configured to be a connection target only.

TOP


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:

Selected Node

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):

new jsPlumbToolkit.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.

TOP


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:

"selectable": {
  events: {
    tap: (params) => {
      this.toolkit.toggleSelection(params.node);
    }
  }
}

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:

Lasso Select Mode

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.

Lasso Operation

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:

ngAfterViewInit() {
    ...
    this.surface.bind("canvasClick", () => this.surface.getToolkit().clearSelection());
    ...
}
Zoom To Fit

The controls component also provides the "zoom to fit" button (as shown in the component declaration discussed above).

Undo/redo

The controls component attaches an undo-redo to the underlying Toolkit instance, to offer undo/redo support for node, group, port and edge additions and/or removals.

TOP


Dataset Component

This component dumps the current contents of the dataset in syntax highlighted json. This component can be found in the file dataset-component.jsx.

Declaration

import React from 'react';

export class DatasetComponent extends React.Component {

    constructor(props) {
        super(props)
    }

    render() {
        return <div className="jtk-demo-dataset"></div>
    }

    initialize(surface) {
        new jsPlumbSyntaxHighlighter(surface.getToolkit(), ReactDOM.findDOMNode(this));
    }
}

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.