DEMOS
DOCS
FEATURES
DOWNLOAD
PURCHASE
CONTACT
BLOG

Groups (Angular)

This demonstration is a clone of the Groups demo, but using the Toolkit's Angular integration, with Angular CLI.

Groups Demo

package.json

{
    "name": "jsplumb-toolkit-angular",
    "version": "1.3.5",
    "license": "Commercial",
    "scripts": {
        "ng": "ng",
        "start": "ng serve",
        "build": "ng build",
        "prod-build":"ng build --prod --base-href .",
        "lint": "ng lint",
        "tscr":"tsc -traceResolution",
        "tsc":"tsc"
    },
    "private": true,
    "dependencies": {
        "@angular/common": "^4.0.0",
        "@angular/compiler": "^4.0.0",
        "@angular/core": "^4.0.0",
        "@angular/forms": "^4.0.0",
        "@angular/http": "^4.0.0",
        "@angular/platform-browser": "^4.0.0",
        "@angular/platform-browser-dynamic": "^4.0.0",
        "@angular/router": "^4.0.0",
        "core-js": "^2.4.1",
        "rxjs": "^5.1.0",
        "zone.js": "^0.8.5",
        "jsplumbtoolkit": "file:../../jsplumbtoolkit.tgz",
        "jsplumbtoolkittypes":"file:../../types/jsplumbtoolkit",
        "jsplumbtypes":"file:../../types/jsplumb"
    },
    "devDependencies": {
        "@angular/cli": "1.1",
        "@angular/compiler-cli": "^4.0.0",
        "@types/jasmine": "2.5.38",
        "@types/node": "~6.0.60",
        "codelyzer": "~2.0.0",
        "ts-node": "~2.0.0",
        "tslint": "~4.4.2",
        "typescript": "~2.2.0"
    }
}

As with the systemjs-config.js file discussed below, this was taken directly from the Angular Quickstart Guide.

There are three entries specific to jsPlumb:

{
  "jsplumbtoolkit":"file:../../jsplumbtoolkit.tgz",
  "jsplumbtoolkittypes":"file:../../types/jsplumbtoolkit",
  "jsplumbtypes":"file:../../types/jsplumb"
}

The first of these is the jsPlumb Toolkit code. The other two entries link to the Typescript Definition files, which are discussed here.

TOP


Page Setup

CSS

In our index.html we import FontAwesome:

<link href="//netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">

Angular CLI expects a CSS file to be placed at src/styles.css. Out styles.css contains styles for the demo and also imports 3 other css files:

  • syntax-highlighter.css For the json dump underneath the canvas.

  • jsplumbtoolkit-defaults.css Provides sane defaults for the Toolkit. You should start building your app with this in the cascade; you can remove it eventually, of course, but you just need to ensure you have provided values elsewhere in your cascade. Generally the safest thing to do is to just include it at the top of your cascade.

  • jsplumbtoolkit-demo.css Some basic common styles for all the demo pages.

TOP


Typescript Setup

This is the tsconfig.json file used by this demonstration:

{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "moduleResolution": "node",
    "sourceMap": true,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "lib": [ "es2015", "dom" ],
    "noImplicitAny": true,
    "suppressImplicitAnyIndexErrors": true

  },
  "include":[ "src/**/*" ]
}

The two things to note here are that we use commonjs as the module type, and we have moduleResolution set to node. Both of these settings are required by jsPlumb.

TOP


Templates

Templates are stored in the templates directory as HTML files.

This is the template used to render Nodes:

<div>
    <div class="name">
        <div class="delete" title="Click to delete" (click)="remove(node)">
            <i class="fa fa-times"></i>
        </div>
        <span></span>
    </div>
    <div class="connect"></div>
    <jtk-source filter=".connect"></jtk-source>
    <jtk-target></jtk-target>
</div>

This is the template used to render Groups:

<div>
    <div class="group-title">

        <button class="expand" (click)="toggleGroup(node)"></button>
    </div>
    <div jtk-group-content="true"></div>
</div>

Group templates can have arbitrary markup as with Node templates. By default, the DOM element representing any Node that is a child of the Group will be appended to the Group's root element. You can, however, mark a place in the Group element that should act as the parent of Node elements - by setting jsplumb-group-content="true" on the element you wish to use. In this demo we use that concept to provide a title bar for each Group onto which Node elements can never be dragged.

These templates are written in Angular's template format.

TOP


Data Loading

This is the data used by this demonstration:

{
    "groups":[
        {"id":"one", "title":"Group 1", "left":100, top:50 },
        {"id":"two", "title":"Group 2", "left":450, top:250, type:"constrained"  }
    ],
    "nodes": [
        { "id": "window1", "name": "1", "left": 10, "top": 20, group:"one" },
        { "id": "window2", "name": "2", "left": 140, "top": 50, group:"one" },
        { "id": "window3", "name": "3", "left": 450, "top": 50 },
        { "id": "window4", "name": "4", "left": 110, "top": 370 },
        { "id": "window5", "name": "5", "left": 140, "top": 150, group:"one" },
        { "id": "window6", "name": "6", "left": 50, "top": 50, group:"two" },
        { "id": "window7", "name": "7", "left": 50, "top": 450 }
    ],
    "edges": [
        { "source": "window1", "target": "window3" },
        { "source": "window1", "target": "window4" },
        { "source": "window3", "target": "window5" },
        { "source": "window5", "target": "window2" },
        { "source": "window4", "target": "window6" },
        { "source": "window6", "target": "window2" }
    ]
}

As with Nodes, if you're using the Absolute layout, you can specify left/top properties for the element.

Additionally, Groups are considered to have a type, just like Nodes, whose default value is default, but which can be overridden in the same way as that of Nodes. Here we see the Group 2 is defined to be of type constrained, which we will discuss in the View section below.

The relationship between Nodes and Groups is written into each Node's data, not into the Group data. Here we see that 4 of the Nodes in our dataset have a group declared.

TOP


Bootstrap

The application is bootstrapped inside src/main.ts:

import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { environment } from './environments/environment';
import { AppModule }              from './app.module';
import { jsPlumbToolkit } from "jsplumbtoolkit";

if (environment.production) {
    enableProdMode();
}

jsPlumbToolkit.ready(() => {
    platformBrowserDynamic().bootstrapModule(AppModule);
});

Here, app is a module defined in src/app.module.ts:

import { NgModule, CUSTOM_ELEMENTS_SCHEMA }      from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent, QuestionNodeComponent, ActionNodeComponent, StartNodeComponent, OutputNodeComponent }  from './app.component';
import { jsPlumbToolkitModule } from "./jsplumbtoolkit-angular";
import { Dialogs } from "jsplumbtoolkit"

@NgModule({
    imports:      [ BrowserModule, jsPlumbToolkitModule ],
    declarations: [ AppComponent, QuestionNodeComponent, ActionNodeComponent, StartNodeComponent, OutputNodeComponent ],
    bootstrap:    [ AppComponent ],
    entryComponents: [ QuestionNodeComponent, ActionNodeComponent, StartNodeComponent, OutputNodeComponent ],
    schemas:[ CUSTOM_ELEMENTS_SCHEMA ]
})
export class AppModule {
    constructor() {
        // initialize dialogs
        Dialogs.initialize({
            selector: ".dlg"
        });
    }
}

TOP


Components

The demo is written as a root level component, which itself uses components from the jsPlumb Angular module.

<jsplumb-toolkit jtk-id="demoToolkit" surface-id="demoSurface" style="width:750px;height:600px;position:relative;margin-right: 20px;">

    ...

    <!-- miniview -->
    <jsplumb-miniview surface-id="flowchartSurface" class="miniview"></jsplumb-miniview>

</jsplumb-toolkit>

Points to note:

  • Both params (for the Toolkit constructor) and renderParams (for the renderer) are provided.
  • An ID is assigned to the Toolkit and to the surface
  • An init method is declared; this will be executed after the Toolkit and Surface have been created.

Here is how the palette (the set of nodes that can be dragged into the work area) is declared:

<div class="sidebar node-palette" jsplumb-palette selector="li" surface-id="flowchartSurface" type-extractor="DemoController.typeExtractor">
    <ul ng-repeat="node in nodeTypes">
        <li jtk-node-type="{{ node.type }} " title="Drag to add new">
            {{node.label}} 
        </li>
    </ul>
</div>

Points to note:

  • nodeTypes is an object in the scope of DemoController.
  • surface-id maps to the surface-id specified in the jsplumb-toolkit directive.
  • The selector attribute instructs the Palette to look for any li elements that are descendants of the Palette's element.
  • A type-extractor function is specified; this is what determines the type of newly dropped elements.

TOP


Toolkit Parameters

The Toolkit instance is created with a groupFactory and a nodeFactory; these are the functions used when a new Group or Node is created after the user drags something on to the canvas:

toolkitParams = {
    groupFactory:(type:string, data:any, callback:Function) => {
        data.title = "Group " + (toolkit.getGroupCount() + 1);
        callback(data);
    },
    nodeFactory:(type:string, data:any, callback:Function) => {
        data.name = (toolkit.getNodeCount() + 1);
        callback(data);
    }
};

TOP


Surface Parameters

Render params are declared as a member of the demo component, and referenced in the jsplumb-toolkit component declaration in the demo component's template:

renderParams = {
    layout:{
        type:"Absolute"
    },
    events: {
        canvasClick: (e:Event) => {
            this.toolkitComponent.toolkit.clearSelection();
        },
        modeChanged:(mode:string) => {
            var controls = document.querySelector(".controls");
            jsPlumb.removeClass(controls.querySelectorAll("[mode]"), "selected-mode");
            jsPlumb.addClass(controls.querySelectorAll("[mode='" + mode + "']"), "selected-mode");
        }
    },
    jsPlumb: {
        Anchor:"Continuous",
        Endpoint: "Blank",
        Connector: [ "StateMachine", { cssClass: "connectorClass", hoverClass: "connectorHoverClass" } ],
        PaintStyle: { strokeWidth: 1, stroke: '#89bcde' },
        HoverPaintStyle: { stroke: "orange" },
        Overlays: [
            [ "Arrow", { fill: "#09098e", width: 10, length: 10, location: 1 } ]
        ]
    },
    lassoFilter: ".controls, .controls *, .miniview, .miniview *",
    dragOptions: {
        filter: ".delete *"
    },
    consumeRightClick:false
};
layout

Parameters for the layout. Here we specify an Absolute layout. It is the only layout currently that supports Groups.

lassoFilter

This selector specifies elements on which a mousedown should not cause the selection lasso to begin. In this demonstration we exclude the buttons in the top left and the miniview.

events

We listen for two events:

canvasClick - a click somewhere on the widget's whitespace. Then we clear the Toolkit's current selection.

modeChanged - Surface's mode has changed (either "select" or "pan"). We update the state of the buttons.

dragOptions

We define a filter for elements that should not cause a node to be dragged.

jsPlumb

Recall that the Surface widget is backed by an instance of jsPlumb Community Edition. This parameter sets the Defaults for that object.

TOP


View

view = {
    nodes:{
        "default":{
            template:"node"
        }
    },
    groups:{
        "default":{
            template:"group",
            endpoint:"Blank",
            anchor:"Continuous",
            revert:false,
            orphan:true,
            constrain:false
        },
        constrained:{
            parent:"default",
            constrain:true
        }
    }
};

The single Node mapping is the most basic Node mapping possible; Nodes derive their Anchor and Endpoint definitions from the jsPlumb params passed in to the Surface parameters discussed above.

The Group mappings, though, bear a little discussion. First, the default Group mapping:

  • template - ID of the template to use to render Groups of this type
  • endpoint - Definition of the Endpoint to use for Connections to the Group when it is collapsed.
  • anchor - Definition of the Anchor to use for Connections to the Group when it is collapsed.
  • revert - Whether or not to revert Nodes back into the Group element if they were dropped outside.
  • orphan - Whether or not to remove Nodes from the Group if they were dragged outside of it and dropped. In actual fact we did not need to set revert to false if orphan is set to true, but in this demo we included all the possible flags just for completeness.
  • constrain - Whether or not to constrain Nodes from being dragged outside of the Group's element.

The constrained Group mapping is declared to extend default, so it gets all of the values defined therein, but it overrides constrain to be true: Nodes cannot be dragged out of the Group element for this type of Group (in this demo we set Group 2 to be of type constrained, and Group 1 - and any Groups dragged on - to be of type default).

TOP


Initialisation

The ngAfterViewInit method of the demo component looks like this:

const toolkit = this.toolkitComponent.toolkit;
const surface = this.toolkitComponent.surface;

const controls = document.querySelector(".controls");
// pan mode/select mode
jsPlumb.on(controls, "tap", "[mode]",  function() {
    surface.setMode(this.getAttribute("mode"));
});

// on home button click, zoom content to fit.
jsPlumb.on(controls, "tap", "[reset]", () => {
    toolkit.clearSelection();
    surface.zoomToFit();
});

// this is just for the demo, to remind people to run npm install
const buildme = document.getElementById("buildme");
buildme.parentNode.removeChild(buildme);

// ---------------- update data set -------------------------
var _syntaxHighlight = function (json:string) {
    json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
    return "<pre>" + json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
        var cls = 'number';
        if (/^"/.test(match)) {
            if (/:$/.test(match)) {
                cls = 'key';
            } else {
                cls = 'string';
            }
        } else if (/true|false/.test(match)) {
            cls = 'boolean';
        } else if (/null/.test(match)) {
            cls = 'null';
        }
        return '<span class="' + cls + '">' + match + '</span>';
    }) + "</pre>";
};

toolkit.load({
    data : {
        "groups":[
            {"id":"one", "title":"Group 1", "left":100, top:50 },
            {"id":"two", "title":"Group 2", "left":450, top:250, type:"constrained"  }
        ],
        "nodes": [
            { "id": "window1", "name": "1", "left": 10, "top": 20, group:"one" },
            { "id": "window2", "name": "2", "left": 140, "top": 50, group:"one" },
            { "id": "window3", "name": "3", "left": 450, "top": 50 },
            { "id": "window4", "name": "4", "left": 110, "top": 370 },
            { "id": "window5", "name": "5", "left": 140, "top": 150, group:"one" },
            { "id": "window6", "name": "6", "left": 50, "top": 50, group:"two" },
            { "id": "window7", "name": "7", "left": 50, "top": 450 }
        ],
        "edges": [
            { "source": "window1", "target": "window3" },
            { "source": "window1", "target": "window4" },
            { "source": "window3", "target": "window5" },
            { "source": "window5", "target": "window2" },
            { "source": "window4", "target": "window6" },
            { "source": "window6", "target": "window2" }
        ]
    },
    onload:function() {
        surface.centerContent();
        surface.repaintEverything();
    }
});

TOP


Behaviour

There are two pieces of behaviour that we need to code that are not completely handled for us by the Toolkit:

  • Delete Node
  • Collapse/Expand Group

which is to say, the Toolkit's API provides functions to call to do these things, but there is no automatic binding of these functions to elements in the UI.

Delete Node
<div class="delete" title="Click to delete" (click)="remove(obj)">
    <i class="fa fa-times"></i>
</div>
remove(obj) {
    this.toolkit.removeNode(obj);
};

remove, which is a method on the NodeComponent, makes a direct call to the removeNode method on the underlying Toolkit instance.

Collapse/Expand Group
<button class="expand" (click)="toggleGroup(obj)"></button>
toggleGroup(group) {
    this.surface.toggleGroup(group);
};

toggleGroup is a method on the GroupComponent.

TOP


Selecting Nodes

Lasso selection is enabled by default on the Surface widget. To activate the lasso, click the pencil icon in the toolbar:

Lasso Select Mode

The code that listens to clicks on this icon is as follows:

// pan mode/select mode
jsPlumb.on(".controls", "tap", "[mode]", function () {
  renderer.setMode(this.getAttribute("mode"));
});

The tap listener extracts the desired mode from the button that was clicked and sets it on the renderer. This causes a modeChanged event to be fired, which is picked up by the modeChanged event listener in the View.

Note that here we could have used a click listener, but tap works better for mobile devices.

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.

Exiting Select Mode

The Surface widget automatically exits select mode once the user has selected something. In this application we also listen to clicks on the whitespace in the widget and switch back to pan mode when we detect one. This is the events argument to the render call:

events: {
  canvasClick: function (e) {
    toolkit.clearSelection();
  }
}

clearSelection clears the current selection and switches back to Pan mode.

TOP


Adding New Nodes/Groups

As discussed above, a jsplumb-palette is declared, which configures all of its child li elements to be droppable onto the Surface canvas. When a drop occurs, the type of the newly dragged node is calculated by the typeExtractor function declared on DemoController:

this.typeExtractor = function (el) {
    return el.getAttribute("jtk-node-type");
};

For a detailed discussion of this functionality, see this page.

TOP


Deleting Nodes

Single Node

Clicking the X button in this demonstration deletes the current node.

jsPlumb.on("#canvas", "tap", ".delete *", function (e) {
  var info = toolkit.getObjectInfo(this);
  toolkit.removeNode(info.obj);
});

TOP


Collapsing/Expanding Groups

Clicking the - button in this demonstration collapses a Group. It then changes to a +, which, when clicked, expands the Group.

jsPlumb.on(canvasElement, "tap", ".group-title button", function(e) {
    var info = toolkit.getObjectInfo(this);
    if (info.obj) {
        renderer.toggleGroup(info.obj);
    }
});

The label of the button is changed via css: when a group is collapsed, it is assigned the CSS class jsplumb-group-collapsed. In the CSS for this demo we have these rules:

.group-title button:after {
    content:"-";
}

.jtk-group.jtk-group-collapsed .group-title button:after {
    content:"+";
}

Another point to note is that the Toolkit does not take any specific action to "collapse" your Groups visually. It is left up to you to respond to the jsplumb-group-collapsed class as you need to. In this demo, we simply hide the group content area:

.jtk-group.jtk-group-collapsed [jtk-group-content] {
    display:none;
}

TOP