Skip to main content

Flowchart Builder (Vue 2)

This Vue 2 app was created with Vite. The code is very similar to the code in the vanilla flowchart builder, but it's just arranged slightly differently. The biggest difference is in the node rendering - in the Toolkit's Vue 2 integration we render each node as a component. We also use components from the Toolkit's vue 2 integration for the miniview and pan/zoom controls.

On this page we discuss the Vue 2 implementation. There are implementations available for each of our library integrations, and also for vanilla JS:

The source code for this application is available on Github at https://github.com/jsplumb/jsplumbtoolkit-applications.

Overview

The main features of this application are:

  • A drawing canvas which renders nodes as SVG shapes, and allows a user to resize nodes. Nodes are dragged on a grid, and resizing is constrained to the same grid.

  • A palette of SVG shapes which can be dragged onto the canvas.

  • Editable edge paths, labels and colors

  • Editable node labels, fill/outline color and text color

  • A miniview

  • A set of controls for managing zoom, undo, redo, etc.

Note that the code snippets on this page refer to constants in many places, which, for the sake of brevity, we have not included. You can find these in the source code on Github.

Throughout the code you will see references to a variable called anchorPositions. This is an array of anchor positions that dictate where on each node edges can be connected. It is declared at the start of the demonstration code:

const anchorPositions = [
{ x:0, y:0.5, ox:-1, oy:0, id:"left" },
{ x:1, y:0.5, ox:1, oy:0, id:"right" },
{ x:0.5, y:0, ox:0, oy:-1, id:"top" },
{ x:0.5, y:1, ox:0, oy:1, id:"bottom" }
]

Each anchor position has x, y, ox and oy values - see anchors for details of this. In addition, we set an id on each anchor, which is not part of the data model but rather is used for CSS purposes. This is discussed below.

Imports


"dependencies":{
"@jsplumbtoolkit/browser-ui-vue2":"^6.2.0"
}

Setup

main.js

We import jsPlumbToolkitVue2Plugin, which contains the components for the surface, miniview and drag/drop palette, as well as ShapeLibraryPlugin, for rendering SVG shapes, and InspectorPlugin, for rendering a node/edge inspector.

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

import { JsPlumbToolkitVue2Plugin, ShapeLibraryPlugin, InspectorPlugin } from '@jsplumbtoolkit/browser-ui-vue2'

import './assets/main.css'

// import Toolkit plugin
Vue.use(JsPlumbToolkitVue2Plugin)
Vue.use(ShapeLibraryPlugin)
Vue.use(InspectorPlugin)

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

Flowchart component

The app's entry point is Flowchart.vue. The template for this is:

<div id="app">

<ControlsComponent surface-id="surfaceId"/>

<div class="jtk-demo-canvas">

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

</jsplumb-toolkit>

<!-- miniview -->
<jsplumb-miniview surface-id="surfaceId"></jsplumb-miniview>
</div>
<div class="jtk-demo-rhs">
<!-- the node palette -->
<div class="sidebar node-palette">
<jsplumb-shape-palette surface-id="surfaceId"
:shape-library="shapeLibrary"
:data-generator="dataGenerator"/>
</div>
<!-- node/edge inspector -->
<InspectorComponent surface-id="surfaceId" v-bind:edge-mappings="edgeMappings"/>

<div class="description">
<p>
This sample application is a builder for flowcharts.
</p>
</div>
</div>
</div>

Note how the various jsplumb- components all take a surface-id as input. The Vue 2 integration offers method to manage the creation of, and access to, surfaces.

Rendering the canvas

The canvas is rendered via the jsplumb-toolkit element shown above. We give it the ID of the surface to create, and we store it on a ref. We plug in a view, some render-params and some toolkit-params, and in this app we also provide the url for a dataset to initially load.

View params

The view is returned as part of the Flowchart's data() value:

view:{
nodes:{
default:{
component:NodeComponent,
events: {
[EVENT_TAP]: (params) => {
edgeEditor.stopEditing()
// if zero nodes currently selected, or the shift key wasnt pressed, make this node the only one in the selection.
if (toolkit.getSelection()._nodes.length < 1 || params.e.shiftKey !== true) {
toolkit.setSelection(params.obj)
} else {
// if multiple nodes already selected, or shift was pressed, add this node to the current selection.
toolkit.addToSelection(params.obj)
}
}
},
inject:{
shapeLibrary:shapeLibrary,
anchorPositions:anchorPositions
},
anchorPositions,
maxConnections:-1
}
},
edges: {
[DEFAULT]: {
endpoint:BlankEndpoint.type,
connector: {
type:OrthogonalConnector.type,
options:{
cornerRadius: 3,
alwaysRespectStubs:true
}
},
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
label:"{{label}}",
outlineWidth:10,
events: {
click:(p) => {
toolkit.setSelection(p.edge)
edgeEditor.startEditing(p.edge, {
deleteButton:true
})
}
}
}
}
}

This view is almost identical to the view in the render call for the vanilla app. The key difference is that in the Vue 2 integration nodes are rendered by Vue components.

Render params

renderParams:AngularRenderOptions = {
// see `edge-mappings.js` for details.
propertyMappings:{
edgeMappings:edgeMappings()
},
// Layout the nodes using an absolute layout
layout: {
type: AbsoluteLayout.type
},
grid:{
size:GRID_SIZE
},
events: {
[EVENT_CANVAS_CLICK]: (e) => {
this.toolkit.clearSelection()
this.edgeEditor.stopEditing()
}
},
consumeRightClick: false,
dragOptions: {
filter: ".jtk-draw-handle, .node-action, .node-action i"
},
plugins:[
{
type:DrawingToolsPlugin.type,
options:{
widthAttribute:"width",
heightAttribute:"height"
}
},
{
type:LassoPlugin.type,
options: {
lassoInvert:true,
lassoEdges:true
}
},
{
type:BackgroundPlugin.type,
options:GRID_BACKGROUND_OPTIONS
}
],
zoomToFit:true
}

The render params are almost identical to the render params for the vanilla app, with one notable exception being that in the Angular app we use a jsplumb-miniview component inside AppComponent, whereas in the vanilla app we configure a miniview as a plugin. Another difference is that we do not have a modelEvents section in the render params for the Angular app, because we write that logic directly into the node component.

Toolkit params

These are injected into the jsplumb-surface component.

toolkitParams = {
// set the Toolkit's selection mode to 'isolated', meaning it can select a set of edges, or a set of nodes, but it
// cannot select a set of nodes and edges. In this demonstration we use an inspector that responds to events from the
// toolkit's selection, so setting this to `isolated` helps us ensure we dont try to inspect edges and nodes at the same
// time.
selectionMode:SelectionModes.isolated,
// This is the payload to set when a user begins to drag an edge - we return values for the
// edge's label, color and line style. If you wanted to implement a mechanism whereby you have
// some "current style" you could update this method to return some dynamically configured
// values.
beforeStartConnect:(node, edgeType) => {
return {
[PROPERTY_LABEL]:"",
[PROPERTY_COLOR]:DEFAULT_STROKE,
[PROPERTY_LINE_STYLE]:EDGE_TYPE_TARGET_ARROW
}
}
}

Nodes

Rendering Nodes

In the viewParams above, we map a single node type DEFAULT to an angular component:

nodes: {
[DEFAULT]:{
component:NodeComponent,
events: {
[EVENT_TAP]: (params) => {
this.edgeEditor.stopEditing()
// if zero nodes currently selected, or the shift key wasnt pressed, make this node the only one in the selection.
if (this.toolkit.getSelection()._nodes.length < 1 || params.e.shiftKey !== true) {
this.toolkit.setSelection(params.obj)
} else {
// if multiple nodes already selected, or shift was pressed, add this node to the current selection.
this.toolkit.addToSelection(params.obj)
}
}
}
}
}

The component's code looks like this:

import {Component} from "@angular/core"
import {BaseNodeComponent} from "@jsplumbtoolkit/browser-ui-angular"

import { anchorPositions } from "./app.component"

@Component({
template:`<div style="width:{{obj.width}}px;height:{{obj.height}}px;color:{{obj.textColor}}" class="flowchart-object" data-jtk-target="true" data-jtk-target-port-type="target">
<span>{{obj.text}}</span>
<jtk-shape [obj]="obj" [width]="obj.width" [height]="obj.height"></jtk-shape>

<div *ngFor="let anchor of anchorPositions"
class="jtk-connect jtk-connect-{{anchor.id}}"
[attr.data-jtk-anchor-x]="anchor.x"
[attr.data-jtk-anchor-y]="anchor.y"
[attr.data-jtk-orientation-x]="anchor.ox"
[attr.data-jtk-orientation-y]="anchor.oy"
data-jtk-source="true"></div>

<div class="node-delete node-action delete" (click)="this.removeNode()"></div>
</div>`
})
export class NodeComponent extends BaseNodeComponent {
anchorPositions = anchorPositions
}

Points to note:

  • We write out values for width, height and color into the root element's style. In this app, each node's backing data contains values for width and height, and we use a Drawing Tools plugin to allow the user to resize elements.
  • We use a jtk-shape component to write out the SVG for each node. See below.
  • We import anchorPositions from the main component and iterate through its values to write out a set of elements from which connections can be dragged
  • We use the removeNode() method from the Toolkit's BaseNodeComponent to remove a node from the dataset

Shape library

This application uses a shape library to support rendering SVG into node elements.

Configuration for this comes in two parts:

Instantiating a shape library

We do this in the AppComponent constructor:


import { FLOWCHART_SHAPES } from "@jsplumbtoolkit/browser-ui"

constructor(public $jsplumb:jsPlumbService) {
this.$jsplumb.registerShapeLibrary([FLOWCHART_SHAPES])
}

registerShapeLibrary is a method in the Toolkit's angular integration which creates a shape library in the Toolkit's angular service, which other components, such as a node palette can connect to.

In this app we use registerShapeLibrary without providing an ID for the library, as we only have one shape library in the app. But we could provide an ID as the second argument, allowing us to support multiple shape libraries:

this.$jsplumb.registerShapeLibrary([FLOWCHART_SHAPES], "flowchartShapeLibrary")
this.$jsplumb.registerShapeLibrary([otherShapes], "otherShapeLibrary")

Rendering shapes

Shapes are rendered with the jtk-shape component:


Dragging new nodes

This application makes use of a shape library to render SVG into the nodes, and also to support a palette of SVG shapes from which the user can drag new nodes to the canvas, and a jtk-shape-palette component to support dragging new nodes.

The jtk-shape-palette is declared in the template for AppComponent:

<jtk-shape-palette surfaceId="surface" [dataGenerator]="dataGenerator"></jtk-shape-palette>

In the shape library section above, we mentioned that the registerShapeLibrary method takes an optional second argument that identifies the shape library, and if this is omitted, a default ID is used instead. The jtk-shape-palette component also supports this concept: in the HTML shown above, notice that we do not provide an ID for the shape library we want this component to render, so it uses the default value.

We could provide an ID if we wanted to:

<jtk-shape-palette shapeLibraryId="otherShapeLibrary" surfaceId="surface" [dataGenerator]="dataGenerator"></jtk-shape-palette>

Data generator

The jtk-shape-palette component is based on the surface drop manager component, and uses the same mechanism to get an initial payload for any element that is being dragged out of the palette - the dataGenerator.

In this app, dataGenerator is a method declared on the AppComponent class:

/**
* Generator for data for nodes dragged from palette.
* @param el
*/
dataGenerator = (el:Element) => {
return {
fill:DEFAULT_FILL,
outline:DEFAULT_STROKE,
textColor:DEFAULT_TEXT_COLOR,
outlineWidth:DEFAULT_OUTLINE_WIDTH
}
}

Resizing nodes

Node resize is supported via the inclusion of the drawing tools plugin, in the renderParams:

plugins:[
{
type:DrawingToolsPlugin.type,
options:{
widthAttribute:"width",
heightAttribute:"height"
}
}
]

In this application we set widthAttribute and heightAttribute because their defaults are w and h, and we are transitioning the Toolkit's UI code over to use width and height throughout the code, because those are the variable names used in the DOM. In 7.x the drawing tools plugin will be updated to use width and height by default.


Editing nodes

Nodes are edited via the InspectorComponent, which is an Angular component that internally creates an Inspector. The after view init method of this component looks like this:

ngAfterViewInit(): void {
this.$jsplumb.getSurface(this.surfaceId, (surface) => {
this.inspector = new Inspector({
container:this.el.nativeElement,
surface,
renderEmptyContainer:() => {
this.currentType = ''
},
refresh:(obj:Base, cb:() => void) => {
this.currentType = obj.objectType
setTimeout(cb, 0)
this.changeDetector.detectChanges()
}
})
})
}

The call to create a new inspector takes four arguments:

  • container the DOM element the inspector should use to apply/extract values. Here it is the component's native element.
  • surface The surface to attach the inspector to. We extract this from the Toolkit's service using the same surface name as we used to instantiate the jsplumb-surface component.
  • renderEmptyContainer The inspector calls this method when nothing is selected. Here, we set the currentType member to an empty string, which will result in the template rendering an empty div element (see below).
  • refresh The inspector calls this method when it needs this component to render an appropriate UI for a newly selected object (or objects). This method passes in the selected object and a callback to invoke once the UI is ready. We set currentType to the object's type, and then we set a timeout which will invoke the callback after this tick is complete. Lastly we instruct the change detector to run. When the timeout fires and the callback is invoked the UI will have been updated.

Edges

Rendering edges

Edges are rendered via the DEFAULT mapping in the edges section of the viewParams shown above. For more information on views, see this page.

edges: {
[DEFAULT]: {
endpoint:BlankEndpoint.type,
connector: {
type:OrthogonalConnector.type,
options:{
cornerRadius: 3,
alwaysRespectStubs:true
}
},
cssClass:CLASS_FLOWCHART_EDGE,
labelClass:CLASS_EDGE_LABEL,
label:"{{label}}",
events: {
[EVENT_CLICK]:(p) => {
this.toolkit.setSelection(p.edge)
this.edgeEditor.startEditing(p.edge, {
deleteButton:true
})
}
}
}
}

One particular point to note here is the EVENT_CLICK handler:

[EVENT_CLICK]:(p) => {
this.toolkit.setSelection(p.edge)
this.edgeEditor.startEditing(p.edge, {
deleteButton:true
})
}

This method does two things:

  • it sets the clicked edge to be the currently selected object in the Toolkit, which the attached inspector will detect, and then display an editor for the edge
  • it calls startEditing on the edge path editor, requesting a deleteButton be attached. Prior to version 6.3.0 you will want to pass anchorPositions in here, as the path editor did not automatically discover them. But from 6.3.0 onwards the path editor will automatically discover the anchor positions.

Edge overlays

The default edge type mapping declares a label:

label:"{{label}}"

This instructs the Toolkit to extract label from each edge's backing data and use it to display a label overlay (at location 0.5). The other overlays you see in this app are achieved via the use of edge property mappings to render different types of edges.

We support 5 types - a plain edge, a dashed edge, an edge with an arrow at the source, an edge with an arrow at the target, and an edge with an arrow at both ends.

These types are declared in the edge-mappings.ts file in the source:

export default function edgeMappings(arrowWidth, arrowLength) {

arrowWidth = arrowWidth || ARROW_WIDTH
arrowLength = arrowLength || ARROW_LENGTH

return [
{
property:PROPERTY_LINE_STYLE,
mappings:{
[EDGE_TYPE_SOURCE_ARROW]:{
overlays:[ { type:ArrowOverlay.type, options:{location:0, direction:-1, width:arrowWidth, Length:arrowLength} } ]
},
[EDGE_TYPE_TARGET_ARROW]:{
overlays:[ { type:ArrowOverlay.type, options:{location:1, width:arrowWidth, length:arrowLength} } ]
},
[EDGE_TYPE_BOTH_ARROWS]:{
overlays:[ {
type:ArrowOverlay.type,
options:{
location:1,
width:arrowWidth,
length:arrowLength
}
}, {
type:ArrowOverlay.type,
options:{
location:0,
direction:-1,
width:arrowWidth,
length:arrowLength
}
} ]
},
[EDGE_TYPE_PLAIN]:{},
[EDGE_TYPE_DASHED]:{
cssClass:CLASS_DASHED_EDGE
}
}
}
]

}

and are set on the surface in the render params.

Default edge type

The beforeStartConnect method that we pass in to the Toolkit's constructor is the means we use to set the default payload for a new edge dragged by the user:

// This is the payload to set when a user begins to drag an edge - we return values for the
// edge's label, color and line style. If you wanted to implement a mechanism whereby you have
// some "current style" you could update this method to return some dynamically configured
// values.
beforeStartConnect:(node, edgeType) => {
return {
[PROPERTY_LABEL]:"",
[PROPERTY_COLOR]:DEFAULT_STROKE,
[PROPERTY_LINE_STYLE]:EDGE_TYPE_TARGET_ARROW
}
}

Editing edges

Edge label, color and "line style" are also edited via the FlowchartInspector: it contains a template for both nodes and for edges, and picks the appropriate one when something is selected.

Edge label and color are both set/changed by making use of the simple edge styles concept, in which there is a basic set of properties that the Toolkit will use to automatically set certain parts of the appearance of an edge.

The "line style" of an edge in this demonstration is a reference to what, if any, overlays the edge has, and whether it is a dashed line or not. As mentioned in the edge rendering section above, this is accomplished by edge property mappings.

In the edge inspector of this demonstration you will see a Line style section:

We use the EdgeTypePicker tag, discussed here to achieve this. The code that does this - and which uses the tag in the edge inspector template - is all in flowchart-inspector.js.


Editing edge paths

Paths can be edited in this application, via an EdgePathEditor. By default (since 6.2.11) this is created in active mode:

edgeEditor = new EdgePathEditor(renderer, {activeMode: true})

..but we leave the activeMode setter in the code to allow for versions prior to 6.2.11.

The call shown above creates an edge path editor but does not activate it. The path editor is activated in the click handler for edges:

edgeEditor.startEditing(p.edge, {
// Show a delete button.
deleteButton:true
})

Background grid

The application renders a background grid via the background plugin. The options we pass (shown above as GRID_BACKGROUND_OPTIONS) are:

{
dragOnGrid:true,
showGrid:true,
showBorder:false,
autoShrink:true,
minWidth:10000,
maxWidth:null,
minHeight:10000,
maxHeight:null,
showTickMarks:false,
type:GeneratedGridBackground.type
}

There is a discussion of each of these options on the background plugin page.


Miniview

We use a miniview in this starter app, via the jsplumb-miniview component that ships with the angular integration.

The miniview component is declared in the template for our app component. We pass in the ID of the surface to which to attach:


<!-- miniview -->
<jsplumb-miniview surfaceId="surface"></jsplumb-miniview>


Controls

Here we use another component that was created for our demonstrations but which now ships with the Toolkit, as we thought it might be useful for others:

<jsplumb-controls surfaceId="surface"></jsplumb-controls>

CSS

Styles for the app itself are in app.css and styles.css.

styles.css also imports a few of the CSS files that ship with the Toolkit:

@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit.css";
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-connector-editors.css";
@import "../node_modules/@jsplumbtoolkit/browser-ui/css/jsplumbtoolkit-controls.css";

jsplumbtoolkit.css provides basic 'structural' CSS, and it is recommended that you include this stylesheet in your cascade, at least when you first start developing with the Toolkit.

jsplumbtoolkit-connector-editors.css contains a mixture of structural and also theming styles, for use by the EdgePathEditor.

jsplumbtoolkit-controls.css contains styles for the controls component, which was created for our demonstrations but since 6.2.0 has been shipped with the Toolkit as it may be useful for others.