DEMOS
DOCS
FEATURES
DOWNLOAD
PURCHASE
CONTACT
BLOG

Flowchart Builder (Vue 2)

This is a port of the Flowchart Builder application that demonstrates the Toolkit's Vue 2 ES6 module based integration, in a Vue CLI 3 application.

Flowchart Builder Demonstration

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

package.json

This file was created for us by the CLI, to which we then added these entries:

{
    "dependencies":{
        ...
        "jsplumbtoolkit": "file:../../jsplumbtoolkit.tgz",  
        "jsplumbtoolkit-vue2": "file:../../jsplumbtoolkit-vue2.tgz",
        "jsplumbtoolkit-undo-redo": "file:../../jsplumbtoolkit-undo-redo.tgz"
        ...
    }
}

TOP


Setup

As this application was generated by the CLI, the setup was done for us. We just had to add the appropriate entries to package.json.

TOP


Bootstrap

A CLI application is bootstrapped through src/main.js. Ours looks like this:

import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

require('@/assets/css/font-awesome.min.css');
require('@/assets/css/jsplumbtoolkit-defaults.css');
require('@/assets/css/jsplumbtoolkit-demo.css');
require('@/assets/css/app.css');

new Vue({ render: h => h(App) }).$mount('#app')

TOP


Application

The component that acts as the entry point of the application is defined in App.vue, which looks like this:

<template>
  <div id="app">
    <div class="jtk-demo-main" id="jtk-demo-flowchart">
      <div style="display:flex">
        <Palette surface-id="surface" selector="[data-node-type]"/>
        <div id="canvas" class="jtk-demo-canvas">
          <Controls surface-id="surface" />
          <Flowchart surface-id="surface" />
        </div>
      </div>
      <div class="description">
        <p>
          This sample application is a copy of the Flowchart Builder application, using the Toolkit's
          Vue 2 integration components and Vue CLI 3.
        </p>
        <ul>
          <li>Drag new nodes from the palette on the left onto the workspace to add nodes</li>
          <li>Drag from the grey border of any node to any other node to establish a link, then provide a description for the link's label</li>
          <li>Click a link to edit its label.</li>
          <li>Click the 'Pencil' icon to enter 'select' mode, then select several nodes. Click the canvas to exit.</li>
          <li>Click the 'Home' icon to zoom out and see all the nodes.</li>
        </ul>
      </div>
    </div>

  </div>
</template>

<script>

    import Vue from "vue"

    import  { Dialogs, jsPlumbToolkit } from "jsplumbtoolkit"

    import Flowchart from './components/Flowchart.vue'
    import Palette from './components/Palette.vue'
    import Controls from './components/Controls.vue'

    export default {
        name: 'app',
        components: {
            Flowchart, Palette, Controls
        },
        mounted:function() {
            jsPlumbToolkit.ready(() => {
                Dialogs.initialize({
                    selector: ".dlg"
                });
            });
        }
    }
</script>

Points to note:

  • The template uses 3 components that are also declared in this app - Flowchart, Palette and Controls. A discussion of each of these is below.
  • We initialise the Toolkit's Dialogs inside a jsPlumbToolkit.ready(..) function in the mounted callback of this component. You may not wish to use the Toolkit's Dialogs; they are something we created for our demonstrations. It isn't always necessary to wrap their initialisation in a ready(..) call, either, but in this demonstration we load them from an external file called templates.html and need to be sure they have loaded before we try to initialise them.

Flowchart Component

This is where most of the functionality is coordinated. We'll break it up into sections and go through each one.

Template

<template>
    <jsplumb-toolkit 
        ref="toolkitComponent" 
        url="flowchart-1.json" 
        v-bind:render-params="renderParams" 
        v-bind:view="view" 
        id="toolkit" 
        surface-id="surface" 
        v-bind:toolkit-params="toolkitParams">

    </jsplumb-toolkit>
</template>

We use the jsplumb-toolkit component, providing several pieces of information:

  • ref="toolkitComponent" we want to be able to retrieve this component after mounting, as we need a reference to the underlying Toolkit instance for some of our business logic
  • url="flowchart-1.json" we provide the url for an initial dataset to load. This is of course optional.
  • v-bind:render-params="renderParams" These are the parameters passed to the Surface component that renders the dataset. As we will see below, we declare these in the data section of the Flowchart component.
  • v-bind:view="view" This the view passed to the Surface component that renders the dataset. As we will see below, we declare this in the data section of the Flowchart component.
  • id="toolkit" We give an id to the Toolkit instance we are creating, which is optional, and in fact in this app we do not use it.
  • surface-id="surface" We assign an ID to the surface for a couple of reasons: first, we need to nominate which surface to attach our miniview, controls and palette components to. Second, we need to access the surface for some of our app's functionality.
  • v-bind:toolkit-params="toolkitParams" These are the parameters passed to constructor of the Toolkit instance. As we will see below, we declare these in the data section of the Flowchart component.

Script Block

We'll break this up into parts too.

Imports
import Vue from 'vue'
import { jsPlumbToolkit, jsPlumb, Dialogs, DrawingTools, jsPlumbUtil } from 'jsplumbtoolkit'
import { jsPlumbToolkitVue2, Palette } from 'jsplumbtoolkit-vue2'

import StartNode from './StartNode.vue'
import ActionNode from './ActionNode.vue'
import QuestionNode from './QuestionNode.vue'
import OutputNode from './OutputNode.vue'

We import Vue and a few bits and pieces from the Toolkit, and from its Vue integration. We also import each of the components used to render our nodes.

Component Level Functionality

We have a few methods defined in the component's scope - things to perform operations on edges, plus the code we use as our data-model#nodefactory:

let toolkitComponent;
let toolkit;


function maybeRemoveEdge(params) {
    Dialogs.show({
        id: "dlgConfirm",
        data: {
            msg: "Delete Edge"
        },
        onOK: function () {
            params.toolkit.removeEdge(params.edge);
        }
    });
}

function editEdge(params) {
    Dialogs.show({
        id: "dlgText",
        data: {
            text: params.edge.data.label || ""
        },
        onOK: function (data) {
            toolkit.updateEdge(params.edge, {label:data.text});
        }
    });
}

function nodeFactory(type, data, callback)  {
    Dialogs.show({
        id: "dlgText",
        title: "Enter " + type + " name:",
        onOK: function (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.
        }
    });
}
Component Definition

The key pieces to note here are:

  • we've declared toolkitParams, renderParams and view in our data object. You can find a discussion of these concepts in the documentation.
  • we map Vue components to node types in the view
  • we initialise the DrawingTools when the component is mounted.
export default {

    name: 'jsp-toolkit',
    props:["surfaceId"],
    data:() => {
        return {
            toolkitParams:{
                nodeFactory:nodeFactory,
                beforeStartConnect:function(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:"..." };
                }
            },
            renderParams:{
              layout:{
                  type:"Spring"
              },
              jsPlumb:{
                  Connector:"StateMachine",
                  Endpoint:"Blank"
              },
              events:{
                  modeChanged:function (mode) {
                      let controls = document.querySelector(".controls");
                      jsPlumb.removeClass(controls.querySelectorAll("[mode]"), "selected-mode");
                      jsPlumb.addClass(controls.querySelectorAll("[mode='" + mode + "']"), "selected-mode");
                  },
                  edgeAdded:(params) => {
                      if (params.addedByMouse) {
                          editEdge(params);
                      }
                  }
              },
              lassoInvert:true,
              elementsDroppable:true,
              consumeRightClick: false,
              dragOptions: {
                  filter: ".jtk-draw-handle, .node-action, .node-action i"
              }
            },
            view:{
                nodes: {
                    "start": {
                        component:StartNode
                    },
                    "selectable": {
                        events: {
                            tap: (params) => params.toolkit.toggleSelection(params.node)
                        }
                    },
                    "question": {
                        parent: "selectable",
                        component:QuestionNode
                    },
                    "action": {
                        parent: "selectable",
                        component:ActionNode
                    },
                    "output":{
                        parent:"selectable",
                        component:OutputNode
                    }
                },
                // 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": maybeRemoveEdge
                        },
                        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:editEdge
                                    }
                                }
                            ]
                        ]
                    }
                },

                ports: {
                    "start": {
                        edgeType: "default"
                    },
                    "source": {
                        maxConnections: -1,
                        edgeType: "connection"
                    },
                    "target": {
                        maxConnections: -1,
                        isTarget: true,
                        dropOptions: {
                            hoverClass: "connection-drop"
                        }
                    }
                }
            }
        };
    },

    mounted() {

        toolkitComponent = this.$refs.toolkitComponent;
        toolkit = toolkitComponent.toolkit;

        jsPlumbToolkitVue2.getSurface(this.surfaceId, (s) => {
            new DrawingTools({
                renderer: s
            });
        });
    }

}

Node Components

Each of the four node types is rendered with a specific Vue component. With the exception of the StartNode component, they each include the BaseEditableNode mixin, whose definition is:

<script>

    import { Dialogs } from 'jsplumbtoolkit'
    import { BaseNodeComponent } from 'jsplumbtoolkit-vue2'

    export default {
        mixins:[ BaseNodeComponent ],
        methods:{
            edit:function() {
                let node = this.getNode();
                Dialogs.show({
                    id: "dlgText",
                    data: node.data,
                    title: "Edit " + node.data.type + " name",
                    onOK: (data) => {
                        if (data.text && data.text.length > 2) {
                            // if name is at least 2 chars long, update the underlying data and
                            // update the UI.
                            this.updateNode(data);
                        }
                    }
                });
            },
            maybeDelete:function() {
                let node = this.getNode();
                Dialogs.show({
                    id: "dlgConfirm",
                    data: {
                        msg: "Delete '" + node.data.text + "'"
                    },
                    onOK:() => {
                        this.removeNode();
                    }
                });
            }
        }
    }

</script>

It offers 2 common methods - to handle editing of a node's label, and to handle a node's deletion.

Note in the node templates we write a v-pre attibrute on jtk-source and jtk-target elements. This instructs Vue to ignore these; without v-pre Vue would try to render these as Vue components.

StartNode

<template>
    <div v-bind:style="{left:obj.left + 'px', top:obj.top + 'px', width:obj.w + 'px', height:obj.h + 'px'}" class="flowchart-object flowchart-start">
        <div style="position:relative">
            <svg :width="obj.w" :height="obj.h">
                <ellipse :cx="obj.w/2" :cy="obj.h/2" :rx="obj.w/2" :ry="obj.h/2" class="outer"/>
                <ellipse :cx="obj.w/2" :cy="obj.h/2" :rx="(obj.w/2) - 10" :ry="(obj.h/2) - 10" class="inner"/>
                <text text-anchor="middle" :x="obj.w / 2" :y="obj.h / 2" dominant-baseline="central"></text>
            </svg>
        </div>
        <jtk-source port-type="start" filter=".outer" filter-negate="true" v-pre/>
    </div>
</template>

<script>
    export default { }
</script>

ActionNode

<template>
    <div v-bind:style="{left:obj.left + 'px', top:obj.top + 'px', width:obj.w + 'px', height:obj.h + 'px'}" class="flowchart-object flowchart-action">
        <div style="position:relative">
            <div class="node-edit node-action">
                <i class="fa fa-pencil-square-o" v-on:click="edit()"/>
            </div>
            <div class="node-delete node-action">
                <i class="fa fa-times" v-on:click="maybeDelete()"/>
            </div>
            <svg :width="obj.w" :height="obj.h">
                <rect x="0" y="0" :width="obj.w" :height="obj.h" class="outer drag-start"/>
                <rect x="10" y="10" :width="obj.w-20" :height="obj.h-20" class="inner"/>
                <text text-anchor="middle" :x="obj.w/2" :y="obj.h/2" dominant-baseline="central"></text>
            </svg>
        </div>
        <jtk-target port-type="target" v-pre/>
        <jtk-source port-type="source" filter=".outer" v-pre/>
    </div>
</template>

<script>
    import BaseEditableNode from './BaseEditableNode.vue'
    export default {
        mixins:[BaseEditableNode]
    }
</script>

OutputNode

<template>
    <div v-bind:style="{left:obj.left + 'px', top:obj.top + 'px', width:obj.w + 'px', height:obj.h + 'px'}" class="flowchart-object flowchart-output">
        <div style="position:relative">
            <div class="node-edit node-action">
                <i class="fa fa-pencil-square-o" v-on:click="edit()"/>
            </div>
            <div class="node-delete node-action">
                <i class="fa fa-times" v-on:click="maybeDelete()"/>
            </div>
            <svg :width="obj.w" :height="obj.h">
                <rect x="0" y="0" :width="obj.w" :height="obj.h"/>
                <text text-anchor="middle" :x="obj.w/2" :y="obj.h/2" dominant-baseline="central"></text>
            </svg>
        </div>
        <jtk-target port-type="target" v-pre/>
    </div>
</template>

<script>
    import BaseEditableNode from './BaseEditableNode.vue'
    export default {
        mixins:[BaseEditableNode]
    }
</script>

QuestionNode

<template>
    <div v-bind:style="{left:obj.left + 'px', top:obj.top + 'px', width:obj.w + 'px', height:obj.h + 'px'}" class="flowchart-object flowchart-action">
        <div style="position:relative">
            <div class="node-edit node-action">
                <i class="fa fa-pencil-square-o" v-on:click="edit()"/>
            </div>
            <div class="node-delete node-action">
                <i class="fa fa-times" v-on:click="maybeDelete()"/>
            </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'" class="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'" class="inner"/>
                <text text-anchor="middle" :x="obj.w/2" :y="obj.h/2" dominant-baseline="central"></text>
            </svg>
        </div>
        <jtk-target port-type="target" v-pre/>
        <jtk-source port-type="source" filter=".outer" v-pre/>
    </div>
</template>

<script>
    import BaseEditableNode from './BaseEditableNode.vue'
    export default {
        mixins:[BaseEditableNode]
    }
</script>

Drag and Drop new nodes

We declare a Palette component in the Flowchart's template:

<Palette surface-id="surface" selector="[data-node-type]"/>

Palette is a component declared in this application, which uses the Palette mixin from the Toolkit's Vue integration:

<template>
    <div class="sidebar node-palette">
        <div class="sidebar-item" :data-node-type="entry.type" title="Drag to add new" v-for="entry in data">
            <i :class="entry.icon"></i>
        </div>
    </div>
</template>

<script>
    import {Palette} from 'jsplumbtoolkit-vue2'

    export default {
        mixins:[Palette],
        methods:{
            typeExtractor: function (el) {
                return el.getAttribute("data-node-type");
            },
            dataGenerator: function (type) {
                return { w:120, h:80 };
            }
        },
        data:function() {
            return {
                data:[
                    { icon:"icon-tablet", label:"Question", type:"question" },
                    { icon:"icon-eye-open", label:"Action", type:"action" },
                    { type:"output", icon:"icon-eye-open", label:"Output" }
                ]
            };
        }
    }

</script>

Note the typeExtractor and dataGenerator methods: they are used by the mixin to identify what has been dropped.