Integrations / React Integration

React Integration

The jsPlumb Toolkit has several components to assist you to integrate with React. This page provides an overview.

Note The original React integration was moved to the jsplumbtoolkit-react-legacy.tgz package as of 2.3.0. It is now deprecated and will be removed in 2.4.0. For existing users of the Toolkit upgrading to version 2.3.0, see this page if you'd just like to switch your imports to the legacy integration and make no other change.

The fundamental difference between the legacy React integration and the current React integration is in the fact that the legacy integration does not render node/group components as children of the Surface component, but the current integration does. This has two main ramifications:

  • You can now pass props down to node/group components
  • Event binding works from 17.x onwards with the current integration.

The first of these - passing props to node/group components - is not something that every application needs, but which can be useful: you can change the appearance of your elements without also having to manipulate the underlying Toolkit's data model. We've incorporated an example of this in the React Skeleton demonstration. The second of these is slightly more fundamental, and is caused by a change React made in 17.x: the element which the delegated event listener is attached used to be the document, and now its the root element into which the given React tree was rendered. As node/group components are not rendered in the tree by the legacy integration, event bindings on them do not work in 17.x.

Other changes made in 2.3.0 include:

  • the ability to specify arbitrary JSX for some node/group, rather than just a component class reference.

The React integration ships in a separate tar - jsplumbtoolkit-react.tgz. Add it to your package.json alongside the Toolkit dependency declaration:

 {
    ...
    "dependencies":{
        ...
        "jsplumbtoolkit":"file:path/to/jsplumbtoolkit.tgz",
        "jsplumbtoolkit-react":"file:path/to/jsplumbtoolkit-react.tgz"
        ...
    }
    ...
}

TOP


The main component you will use is the JsPlumbToolkitSurfaceComponent.

Let's take a quick look at how you'd use one:

class DemoComponent extends React.Component {

    constructor(props) {
        super(props);
        this.toolkit = jsPlumbToolkit.newInstance({
            ...
        });

        this.view = {
            ...
        }

        this.renderParams = {
            ...
        }
    }

    render() {
        return <div style={{width:"100%",height:"100%"}}>
                    <JsPlumbToolkitSurfaceComponent renderParams={this.renderParams} toolkit={this.toolkit} view={this.view} ref={ (c) => { if (c != null) this.surface = c.surface } }/>
                    <ControlsComponent ref={(c) => this.controls = c }/>
                    <DatasetComponent ref={(d) => this.dataset = d }/>
                    <div className="miniview"/>                        
                </div>
    }
        
}

We create an instance of the Toolkit in the component's constructor, which we then inject into the JsPlumbToolkitSurfaceComponent, along with renderParams and the view.

ControlsComponent is something built for the Toolkit React demonstrations, to handle zoom/selection of nodes. DatasetComponent is also part of this demonstration - it's a React version of the dataset view you may have seen on other demonstrations. Neither of these components ships in the React integration package, but you are welcome to grab the code from one of the demonstrations if you wish to use it.

jsPlumb Components

React is component based. The Toolkit offers 3 components:

JsPlumbToolkitSurfaceComponent

<JsPlumbToolkitSurfaceComponent renderParams={this.renderParams} view={this.view} toolkit={this.toolkit}/>
Attributes
  • toolkit A reference to an instance of the Toolkit. Required.
  • renderParams Parameters to pass in to the constructor of the Surface widget. Optional, but you'll probably supply something here.
  • view View parameters. Views are discussed here. Again, optional, but you'll probably want to supply something.

TOP


JsPlumbToolkitMiniviewComponent

Provides a Miniview that can be attached to some Surface component.

Example

import JsPlumbToolkitMiniviewComponent from 'jsplumbtoolkit-react';

const miniview = ReactDOM.render(
    <JsPlumbToolkitMiniviewComponent surface={JsPlumbToolkitSurfaceComponent}/>, document.querySelector(".miniview")
)
Attributes
  • surface The JsPlumbToolkitSurfaceComponent to which to attach the Miniview component.

TOP


SurfaceDropComponent

Provides a means to implement drag/drop of new Nodes/Groups onto your Surface. This component is abstract; you are expected to provide the render method.

Example

First, declare your subclass and provide the render method:


import SurfaceDropComponent from 'jsplumbtoolkit-react';

class MyPalette extends SurfaceDropComponent {

  render() {
    return <div className="someClass">
                <ul>
                    <li data-node-type="foo" jtk-is-group="true">FOO</li>
                    <li data-node-type="bar">BAR</li>
                </ul>
           </div>
  }
}

Then create one in your app:


const typeExtractor = function(el) { return el.getAttribute("data-node-type") };
const dataGenerator = function (type) { return { w:120, h:80 }; };
const nodePaletteElement = document.querySelector(".parentOfNodePalette")

const nodePalette = ReactDOM.render(
        <MyPalette surface={this.surface} selector={"li"} container={nodePaletteElement} dataGenerator={dataGenerator}/>
, nodePaletteElement);

As with the Miniview component, this component needs a reference to a JsPlumbToolkitSurfaceComponent.

Attributes
  • selector:string A valid CSS3 selector identifying descendant nodes that are to be configured as draggable/droppables.
  • dataGenerator:(el:HTMLElement) => T This Function is used to provide default data for some Node/Group. Note that a difference between this component and the original jsplumb-palette is that your dataGenerator function is now expected to determine the "type" of the object being dragged, and to set it on the data object if desired.
  • surface Required. The JsPlumbToolkitSurfaceComponent to which to attach the Drop Manager.
  • allowDropOnGroup:boolean Optional, defaults to true. If true, then elements can be dropped onto nodes/groups, and in the event that occurs, the onDrop method will be called.
  • allowDropOnCanvas:boolean Optional, defaults to true. When an element is dropped on the canvas whitespace, it is added to the dataset and rendered.
  • allowDropOnEdge:boolean Optional, defaults to true. If true, then elements can be dropped onto edges, and in the event that an element is dropped on an edge, a new node/group is added and inserted between the source and target of the original edge, and the original edge is discarded..
  • typeGenerator:(data:T) => string Optional. A function that can return the correct type for some data object representing an element being dragged. By default the Toolkit will use the type member of the data object.
  • groupIdentifier:(d: T, el: HTMLElement) => boolean Optional. By default, the toolkit looks for a jtk-is-group attribute on an element being dragged. If found, with a value of "true", then the Toolkit assumes a group is being dragged. You can supply your own function to make this decision.

For further reading, see this page .

TOP


Each Node or Group in your UI is rendered as an individual component.

Definition

As an example, consider the component we use to render an Action node in the Flowchart builder demonstration:

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.node.data;

        return <div style={{width:obj.w + 'px', height:obj.h + 'px'}} className="flowchart-object flowchart-action">
            <div style={{position:'relative'}}>
                <svg width={obj.w} height={obj.h}>
                    <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>
            <div className="node-edit node-action" onClick={this.edit.bind(this)}></div>
            <div className="node-delete node-action" onClick={this.remove.bind(this)}></div>
            <div className="drag-start connect"></div>
            <jtk-target port-type="target"/>
            <jtk-source port-type="source" filter=".connect"/>
        </div>
    }
}

Here, BaseEditableComponent is common for all nodes, and offers a few basic methods like node edit/remove. The class declaration looks like this:


import { BaseNodeComponent }  from 'jsplumbtoolkit-react';
import { Dialogs } from 'jsplumbtoolkit';

export class BaseEditableComponent extends BaseNodeComponent {
    
    ...
}

Mapping to a type

Amongst other things, the view is used to map node/group types to their rendering. From 2.3.0 onwards there are two ways to map a node/group type to what gets rendered.

Here's the nodes section from the view in the original React Flowchart Builder application:

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: {
        ...
    }
}

Here, we map node types to various classes. In previous versions, it was imperative that you extended BaseNodeComponent or BaseGroupComponent in the components you used to render nodes/groups. This is no longer the case, but the base components do have a few useful methods. In the React Flowchart Builder demonstration each of the components shown, as discussed above, extend BaseEditableComponent, which itself extends the Toolkit's BaseNodeComponent, and that component extends React.Component.

An alternative to mapping component classes is to provide some JSX. The view shown above actually now looks like this the demonstration code we ship:

this.view = {
    nodes: {
        "start": {
           jsx:(ctx) => { return <StartComponent ctx={ctx} /> }
        },
        "selectable": {
            events: {
                tap:  (params) => {
                    this.toolkit.toggleSelection(params.node);
                }
            }
        },
        "question": {
            parent: "selectable",
            jsx:(ctx) => { return <QuestionComponent ctx={ctx} /> }
        },
        "action": {
            parent: "selectable",
            jsx:(ctx) => { return <ActionComponent ctx={ctx} /> }
        },
        "output":{
            parent:"selectable",
            jsx:(ctx) => { return <OutputComponent ctx={ctx} /> }
        }
    },
    // There are two edge types defined - 'yes' and 'no', sharing a common
    // parent.
    edges: {
        ...
    }
}

When you use the jsx approach, you're expected to provide a function that takes ctx as argument, and returns JSX. If, as in this demonstration, your classes extend BaseNodeComponent (or BaseGroupComponent) you should pass ctx in as a prop, so that the base class can extract the things it needs to provide the various support methods it offers. If your component does not extend one of the Toolkit's base components, though, then obviously passing ctx in as a prop is entirely up to you.

ctx is an object containing five things:

interface JsxContext {
    vertex: Node | Group;           // the vertex - node or group - that is being rendered by this JSX
    surface: Surface;               // the Surface widget that is rendering the vertex
    toolkit: JsPlumbToolkit;        // the underlying JsPlumbToolkit instance
    data: Record<string, any>;      // the vertex's backing data. This is just a convenience - it can be accessed via `vertex.data`
    props: Record<string, any>;     // these are any props that you passed in as the `childProps` argument to a `JsPlumbToolkitSurfaceComponent`. See below.    
}

As mentioned above, you can pass back arbitrary JSX from these functions.

As a convenience, your components can extend BaseNodeComponent. If you use the jsx approach described above, you should pass ctx in as a prop; if you use a class reference via component, the Toolkit will take care of passing everything the base component needs.

BaseNodeComponent offers a couple of useful methods:

  • removeNode() - instructs the underlying Toolkit to remove the Node this component represents. The node will be removed from the model, and the component will be unmounted.
  • updateNode(data:Record<string, any>) - updates the underlying Node's data model. This will result in the component re-rendering.

as well as these properties:

  • node - The Node object the component is rendering
  • surface - The Surface by which the Node was rendered
  • toolkit - The underlying JsPlumbToolkit instance.

This component is conceptually identical to BaseNodeComponent, but with different method names/properties:

  • removeGroup() vs removeNode()
  • updateGroup(...) vs updateNode(...)
  • group vs node

If your UI makes use of Ports, like the Database Visualizer that ships with the Toolkit, you should extend BasePortComponent in your Port components. This is a requirement that may change in a future release. Here's how the table node type from the referenced demonstration writes out elements representing its columns:

<div className="table-columns">
    { this.node.data.columns.map(c => <ColumnComponent data={c} key={c.id} parent={this}/>) }
</div>

Each of the props shown here is required.

  • data - This is the backing data for the Port
  • key - Provide a unique key for each Port, to ensure that React only renders it once. If the Port gets re-rendered the underlying Toolkit renderer will lose the DOM element it is tracking.
  • parent - A reference to the component rendering the Vertex (Node/Group) to which the Port belongs. The BasePortComponent needs this in order to perform some essential housekeeping.

TOP


As shown in the React Skeleton demonstration, you can pass props in to the components used to render your vertices by setting them on the childProps prop of a Surface component. For instance, in the demonstration mentioned, DemoComponent - the main app - declares a random color variable in its initial state:

class DemoComponent extends React.Component {

    constructor(props) {
        super(props);
        this.toolkit = jsPlumbToolkit.newInstance();
        this.state = { color:randomColor() };
       
    }
    
}

In the render method of the DemoComponent, a surface component is created, and the color member of the main component's state is passed in:

render() {
    return <div style={{width:"100%",height:"100%",display:"flex"}}>
        <button onClick={this.changeColor.bind(this)} style={{backgroundColor:this.state.color}} className="colorButton">Change color</button>
        <JsPlumbToolkitSurfaceComponent childProps={{color:this.state.color}} renderParams={this.renderParams} toolkit={this.toolkit} view={this.view} />
    </div>
}

In the view for the DemoComponent, each of the node types references the Surface's childProps by way of the context they are given:

this.view = {
    nodes: {
        "shin":{
            jsx: (ctx) => { return <ShinBoneComponent color={ctx.props.color} ctx={ctx}/> }
        },
        "knee":{
            jsx: (ctx) => { return <KneeBoneComponent color={ctx.props.color} ctx={ctx}/> }
        }
    },
    ...
}

So, here, ctx.props.color is referencing this.state.color in DemoComponent. If we change DemoComponent's state, the change propagates through to the vertex components:

changeColor() {
    const current = this.state.color;
    let col;
    while (true) {
        col = randomColor();
        if (col !== current) {
            break;
        }
    }
    this.setState({ color:col } )
}

You may, if you're a bit React-savvy, be thinking "why not use use this.state.color as the prop for the bone components?", and that's a valid question to ask. The answer is that in this case you seem to be able to, which is good, because that's intuitive. But in other cases the exact same setup was observed to not work, which is bad, as its counter intuitive. Passing everything through the Surface as childProps, though, works in all cases.