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 integration. This page relates to the legacy Vue 2 integration; for a discussion of the newer ES6 module based integration, see demo-vue.

Flowchart Builder Demonstration

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

package.json

{
  "name": "jsplumbtoolkit-vue-demo",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "author": "jsPlumb <hello@jsplumbtoolkit.com> (https://jsplumbtoolkit.com)",
  "license": "Commercial",
  "dependencies": {
    "font-awesome": "^4.7.0",
    "jsplumbtoolkit": "file:../../jsplumbtoolkit.tgz",
    "jsplumbtoolkit-vue2": "file:../../jsplumbtoolkit-vue2-legacy.tgz",
    "vue": "^2.1.10"
  }
}

There are two entries specific to jsPlumb:

"jsplumbtoolkit":"file:../../jsplumbtoolkit.tgz",
"jsplumbtoolkit-vue2":"file:../../jsplumbtoolkit-vue2-legacy.tgz"

We import the jsPlumb Toolkit dependencies via local file references to built packages (which are available in a licensed download).

TOP


Page Setup

CSS
<link href="node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
<link rel="stylesheet" href="node_modules/jsplumbtoolkit/dist/css/jsplumbtoolkit-defaults.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
<script src="node_modules/vue/dist/vue.js"></script>
<script src="node_modules/jsplumbtoolkit/dist/js/jsplumbtoolkit.js"></script>
<script src="node_modules/jsplumbtoolkit-vue2/dist/js/jsplumbtoolkit-vue2.js"></script>
<script src="app.js"></script>

We import Vue, the Toolkit, and the Toolkit's Vue 2 integration. app.js contains the demo code.

TOP


Templates

There are four templates used by the app - one each for the node types of Question, Action and Output, and one for the Start node. These are contained in the templates.html file, which is imported into the page via this script tag:

<script type="text/x-jtk-templates" src="templates.html"></script>

The templates look like this:

Start

<script type="jtk" id="tmplStart">
  <div style="left:${left}px;top:${top}px;width:${w}px;height:${h}px;" class="flowchart-object flowchart-start">
    <div style="position:relative">
      <svg:svg width="${w}" height="${h}">
        <svg:ellipse cx="${w/2}" cy="${h/2}" rx="${w/2}" ry="${h/2}"></svg:ellipse>
        <svg:text text-anchor="middle" x="${ w / 2 }" y="${ h / 2 }" dominant-baseline="central">${text}</svg:text>
      </svg:svg>
    </div>
    <jtk-source port-type="start" filter="svg *" filter-negate="true"></jtk-source>
  </div>
</script>

The Start node consists of an ellipse with a text label centered inside of it. Note here how all SVG elements are required to be declared in the svg: namespace. This is a requirement of Rotors, the Toolkit's default templating engine, and would not necessarily apply if you were using some other template engine.

In this template we can see the w, h, left and top values from the node's data being used not just to position the element but also to provide appropriate values for the ellipse and text label.

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.

Action

<script type="jtk" id="tmplAction">
  <div style="left:${left}px;top:${top}px;width:${w}px;height:${h}px;" class="flowchart-object flowchart-action">
    <div style="position:relative">
      <div class="node-edit node-action">
        <i class="fa fa-pencil-square-o"></i>
      </div>
      <div class="node-delete node-action">
        <i class="fa fa-times"></i>
      </div>
      <svg:svg width="${w}" height="${h}">
        <svg:rect x="0" y="0" width="${w}" height="${h}"></svg:rect>
        <svg:text text-anchor="middle" x="${w/2}" y="${h/2}" dominant-baseline="central">${text}</svg:text>
      </svg:svg>
    </div>
    <jtk-target port-type="target"></jtk-target>
    <jtk-port port-type="source"></jtk-port>
  </div>
</script>

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.

Question

<script type="jtk" id="tmplQuestion">
  <div style="left:${left}px;top:${top}px;width:${w}px;height:${h}px;" class="flowchart-object flowchart-question">
    <div style="position:relative">
      <div class="node-edit node-action">
        <i class="fa fa-pencil-square-o"></i>
      </div>
      <div class="node-delete node-action">
        <i class="fa fa-times"></i>
      </div>
      <svg:svg width="${w}" height="${h}">
        <svg:path d="M ${w/2} 0 L ${w} ${h/2} L ${w/2} ${h} L 0 ${h/2} Z"></svg:path>
        <svg:text text-anchor="middle" x="${w/2}" y="${h/2}" dominant-baseline="central">${text}</svg:text>
      </svg:svg>
    </div>
    <jtk-port port-id="no" port-type="noSource"></jtk-port>
    <jtk-port port-id="yes" port-type="yesSource"></jtk-port>
    <jtk-target port-type="target"></jtk-target>
  </div>
</script>

The Question node draws a diamond, and declares itself to be an edge target. Unlike the Action node, though, this node adds two Endpoints, via jtk-port elements, instead of declaring that the entire element is an edge source.

Output

 <script type="jtk" id="tmplOutput">
  <div style="left:${left}px;top:${top}px;width:${w}px;height:${h}px;" class="flowchart-object flowchart-output">
    <div style="position:relative">
      <div class="node-edit node-action">
        <i class="fa fa-pencil-square-o"></i>
      </div>
      <div class="node-delete node-action">
        <i class="fa fa-times"></i>
      </div>
      <svg:svg width="${w}" height="${h}">
        <svg:rect x="0" y="0" width="${w}" height="${h}"></svg:rect>
        <svg:text text-anchor="middle" x="${w/2}" y="${h/2}" dominant-baseline="central">${text}</svg:text>
      </svg:svg>
    </div>
    <jtk-target port-type="target"></jtk-target>
  </div>
</script>

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

TOP


Vue Setup

Inside app.js we setup a Vue application in this way:

jsPlumbToolkit.ready(function() {

    var demo = new Vue({    
        el: '#jtk-demo-flowchart',\
        data: {
            paletteData:...,
            renderParams:...,
            toolkitParams:...,
            view:...,
            graph:...
        },
        methods:{

            ...
        }
    });

});

We'll go through each of these parameters in turn to explain how the demo is put together. It is possible that there are Vue purists for whom some of our decisions are not optimum - we'd welcome any feedback/suggestions on this.

TOP


Data

Our data section declares five items:

paletteData

[
  { icon:"icon-tablet", label:"Question", type:"question" },
  { icon:"icon-eye-open", label:"Action", type:"action" },
  { type:"output", icon:"icon-eye-open", label:"Output" }
]

This is the list of node types that can be dragged on to the canvas. It is injected into a component we define in this demo that uses the vue2-integration#palette-mixin.

Our palette component is declared inside app.js and looks like this:

Vue.component('jsplumb-palette', {
    props:["data"],
    template:'<div class="sidebar node-palette"><ul><li :jtk-node-type="entry.type" title="Drag to add new" v-for="entry in data"><i :class="entry.icon"></i></li></ul></div>',
    mixins:[jsPlumbToolkit.Vue.Palette],
    methods:{
        typeExtractor: function (el) {
          return el.getAttribute("jtk-node-type");
        },
        dataGenerator: function (type) {
            return { w:120, h:80 };
        }
    }
});

It is instantiated in this demo inside index.html with the following line:

<jsplumb-palette v-bind:data="paletteData" surface-id="surface" selector="[jtk-node-type]"></jsplumb-palette>

renderParams

These are the parameters that will be passed to the constructor of the Surface widget. In the HTML this is wired up in this markup:

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

The jsplumb-toolkit component is discussed here.

In this demo, the render params are:

{
    layout:{
       type:"Spring"
    },
    jsPlumb:{
       Connector:"StateMachine",
       Endpoint:"Blank"
    },
    events:{
       modeChanged:function (mode) {
           var controls = document.querySelector(".controls");
           jsPlumb.removeClass(controls.querySelectorAll("[mode]"), "selected-mode");
           jsPlumb.addClass(controls.querySelectorAll("[mode='" + mode + "']"), "selected-mode");
       },
       canvasClick: function (e) {
           demo.clearSelection();
       }
    },
    lassoInvert:true,
    consumeRightClick: false,
    dragOptions: {
       filter: ".jtk-draw-handle, .node-action, .node-action i"
    }
}
  • layout We use the layout-spring.
  • jsPlumb These are "global" rendering hints for the jsPlumb instance backing the Surface widget.
  • events We wire up two events here: modeChanged, fired when the Surface enters/exits lasso mode, in response to which we change the appearance of the controls buttons, and canvasClick, which is a click on the whitespace in the Surface widget, in response to which we call demo.clearSelection(). This method is discussed in the #methods section below.
  • lassoInvert Instructs the Toolkit to "invert" the lasso: the selected area is unmasked, as opposed to 'normal' lasso operation, in which the selected area is masked.
  • consumeRightClick By default, right clicks are disabled on a Surface. We set this to help people explore what's going on in the demo. In production we generally leave this at the default value of true.
  • dragOptions Options for node dragging.

toolkitParams

These are the parameters passed to the constructor of the jsPlumb Toolkit instance. They are, like renderParams, wired up in the jsplumb-toolkit component declaration in the HTML.

{
    nodeFactory: function (type, data, callback) {
      jsPlumbToolkit.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 = jsPlumbToolkitUtil.uuid();
                      callback(data);
                  }
                  else
                  // else advise the user.
                      alert(type + " names must be at least 2 characters!");
              }
              // else...do not proceed.
          }
      });
  },
  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:"..." };
  }
}
  • nodeFactory This function is called when a new node is dropped onto the canvas. type is the value returned from the typeExtractor function in our palette component. data may be empty, or it may have been pre-populated by a dataGenerator defined on the palette component. The contract here is that if we wish to proceed adding the node we call callback with the data modified as we saw fit; if we do not call callback then the node addition is aborted.

  • beforeStartConnect This is an interceptor that we use to either reject new edges from a Start node that already has one, or to return seed data for an edge we have allowed to proceed. Interceptors are discussed in detail here.

view

The view provides the set of declarations of node, edge and port types (and groups, if you're using them, which we are not in this demo).

{
    nodes: {
      "start": {
          template: "tmplStart"
      },
      "selectable": {
          events: {
              tap: function (params) {
                  demo.toggleSelection(params.node);
              }
          }
      },
      "question": {
          parent: "selectable",
          template: "tmplQuestion"
      },
      "action": {
          parent: "selectable",
          template: "tmplAction"
      },
      "output":{
          parent:"selectable",
          template:"tmplOutput"
      }
  },
  // 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": function (params) {
                  demo.maybeRemoveEdge(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:function(params) {
                          demo.editLabel(params.edge);
                      }
                  }
              }
              ]
          ]
      }
  },

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