Skip to main content

Nodes and Groups

A node represents some entity in your application - examples from the Toolkit's own starter apps are the various actions/questions etc in the flowchart, or a table or view in the schema builder, a step in the chatbot builder, etc.

Groups also represent an entity in your application, and they act as a container for zero or more nodes or other groups. A good example of this are the various groups in our network topology builder - a region, or a VPC, etc. Each of these represents an entity in the application which itself contains other nodes and/or groups.

Rendering nodes and groups

Nodes and groups are rendered either by a template, when using vanilla Toolkit, or they are mapped to a component, if you are using a library integration. Nodes and groups are mapped to a template or component inside a view alongside edges and, sometimes, ports:

toolkit.render({
...
view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`,
events:{
click:function(p) {
alert("You clicked on node " + p.obj.id);
}
}
},
"type1":{
template:`<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`
}
},
groups:{
"default":{
template:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"></div></div>`,
events:{
click:function(p) {
alert("You clicked on group " + p.obj.id);
}
}
}
}
}
});

Here, nodes of type type1 have their own mapping to a template, which is in the format discussed on the templating page. Any other node type is mapped to the "default" mapping, which declares a template and also a click listener. A group of any type maps to the default mapping in the groups section, which provides a template for rendering groups and a click listener.

The full list of options for node and group mappings inside a view can be found in the views documentation.

The syntax for templates when using vanilla Toolkit is discussed on the Templating page. If you're using one of our library integrations you won't be using the Toolkit's vanilla template renderer.

note

The various examples on this page show both nodes and groups, but not every application uses groups. Many applications only use nodes. We include both on this page since the approach to rendering them is exactly the same, although there are various extra options for groups, which are discussed below.

Specifying a template/component

When using vanilla Toolkit you can map either some HTML representing the template to use for a given node or group, as shown above:

view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`
},
"type1":{
template:`<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`
}
},
groups:{
"default":{
template:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"></div></div>`
}
}
}

or you can map a templateId, which the Toolkit will use to resolve the template to use:

view:{
nodes:{
"default":{
templateId:"defaultNodeTemplate"
},
"type1":{
templateId:"type1Template" `<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`
}
},
groups:{
"default":{
templateId:"groupTemplate"
}
}
}
Mapping Components

If you're using a library integration, you'll be mapping components to node/group types, not templates or template IDs as shown here - see the links at the bottom of this page for the integration you're using.

The sections on this page discussing node/group sizing and events are still relevant if you're using a library integration.

Resolving templates by ID

The Toolkit will look in a couple of places to try to resolve a template by its ID. One option you have is to provision the template in the HTML page itself as a script element:

<script type="jtk" id="defaultNodeTemplate">
<div class="aNode">{{id}}</div>
</script>

Note the type="jtk" attribute.

Alternatively you can provide a templates object in the render parameters:

{
view:{
nodes:{
"default":{
templateId:"defaultNodeTemplate"
},
"type1":{
templateId:"type1Template"
}
},
groups:{
"default":{
templateId:"groupTemplate"
}
}
},
templates:{
defaultNodeTemplate:`<div class="aNode">{{id}}</div>`,
type1Template:`<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`,
groupTemplate:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"></div></div>`
}
}

A common strategy we see people employing is to store the templates in a separate JS file as the default export:

export default {
defaultNodeTemplate:`<div class="aNode">{{id}}</div>`,
type1Template:`<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`,
groupTemplate:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"></div></div>`
}

and then importing these:


import templates from "./templates.js"

{
view:{
nodes:{
"default":{
templateId:"defaultNodeTemplate"
},
"type1":{
templateId:"type1Template"
}
},
groups:{
"default":{
templateId:"groupTemplate"
}
}
},
templates
}

Mapping events

You can map a number of events to nodes and groups in a view, and when the event fires, the Toolkit will supply the event from the browser, the DOM element on which the event occurred, and the model object related to the event.

You can bind a listener to anything listed as a BindableEvent:

type BindableEvent = "click" 
| "dblclick"
| "mouseover"
| "mouseout"
| "mousedown"
| "mouseup"
| "tap"
| "dbltap"
| "contextmenu"

To map an event, provide an events object inside a node or group definition:

view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`,
events:{
"tap":(p:{e:MouseEvent, obj:Node}) => {
alert(`You tapped on node ${p.obj.id}`)
},
"contextmenu":(p:{e:MouseEvent, obj:Node}) => {
alert(`You right-clicked on node ${p.obj.id}`)
}
}
}
}
}
caution

Since version 6.6.0, hover events are not enabled by default, due to the overhead incurred from adding these listeners. So mouseover and mouseout will not be fired by default. You can switch these on by setting hoverEvents:true in your render parameters.

Setting node/group size

The default behaviour of the Toolkit is to render nodes and groups using whatever HTML is provided, and then after the element has been rendered, reading back the size of the element from the DOM. For many types of applications this approach is really useful - you can draw whatever you like for your nodes or groups and the Toolkit will figure out where any connected edges need to be placed.

In some applications, though, you'll want to give your users control over the size of nodes/groups, and the Toolkit supports that too. For versions of the Toolkit prior to 6.11.0 the approach to supporting this involves a little manual labour. Firstly, you need to include width and height properties in your node data:

{
id:"someNode",
width:200,
height:150
}

And then you need to reference these values in the style of the template root:

toolkit.render(someElement, {  
view:{
nodes:{
"default":{
template:`<div class="aNode" style="width:{{width}}px;height:{{height}}px">{{id}}</div>`
}
}
}
})

Whenever the width and height properties are updated the element's style will be updated and the element's size will change. You can do the same thing with groups.

Since 6.11.0 we've formalised this a little.

useModelForSizes

From 6.11.0 onwards you can instruct the Toolkit to extract width and height from your node/group data and to set the DOM element to these values, via the useModelForSizes flag:

toolkit.render(someElement, {  
view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`
}
},
groups:{
"default":{
template:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"/></div>`
}
}
},
useModelForSizes:true
})

The Toolkit will now set the width and height of rendered DOM elements from the width and height properties in their data. When either of those values are updated, the Toolkit will update the size of the DOM element accordingly. We use this new functionality in the Flowchart Builder starter app.

Default node/group size

If a given node or group does not have width or height values in its data, the Toolkit will use a default value, which you can specify in one of two places - either the defaults section of a render call:

toolkit.render(someElement, {  
view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`
}
},
groups:{
"default":{
template:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"/></div>`
}
}
},
useModelForSizes:true,
defaults:{
nodeSize:{w:150, h:100},
groupSize:{w:300, h:300}
}
})

or inside a node or group definition in the view:

toolkit.render(someElement, {  
view:{
nodes:{
"default":{
template:`<div class="aNode">{{id}}</div>`,
defaultSize:{w:150, h:100}
},
"type1":{
template:`<div class="aNode"><h1>TYPE 1</h1><p>{{id}}</p></div>`
}
},
groups:{
"default":{
template:`<div class="aGroup"><h1>{{id}}</h1><div data-jtk-group-content="true"/></div>`,
defaultSize:{w:400, h:400}
}
}
},
useModelForSizes:true,
defaults:{
nodeSize:{w:150, h:100},
groupSize:{w:300, h:300}
}
})

You can in fact provide values in both places - as shown above - and the Toolkit will use the values from a node or group definition first. In the above example we see that type1 has no default size set, so the Toolkit will use nodeSize from the defaults block. The default group definition does have a defaultSize, so that will be used for any groups that do not have width or height information in their backing data.

In the absence of any default values, the Toolkit will render nodes with width 100 pixels and height 80 pixels, and groups with a width and height of 300 pixels.

Groups

Groups act as a container for zero or more nodes or other groups. In the UI, these can be collapsed, and edges to/from the nodes/groups inside the group are then temporarily relocated to the group container. There is no limit imposed on how deeply groups may be nested.

Groups have in common with nodes, edges and ports the two concepts of id and of type, and as with the other graph objects, type will be set to "default" if it cannot be determined. Group IDs and types either follow the default rules (ie. they are given by the id and type parameters, respectively, in the group's data), or they are derived by applying the current idFunction and typeFunction.

Groups, as with the other graph objects, can have arbitrary JSON data associated with them.


Rendering

As shown ine various examples at the start of this page, groups are rendered using client side templates just as nodes are. They are declared inside the view alongside nodes, edges and ports, and broadly follow the same syntax as node definitions - but there are a number of extra flags that can be set on a group definition.

A simple example to start:

toolkit.render({
...
view:{
nodes:{
"default":{
templateId:"tmplNode",
events:{
click:function(p) {
alert("You clicked on node " + p.node.id);
}
}
}
},
groups:{
"groupType1":{
templateId:"tmplGroupType1",
constrain:true
}
}
}
});

Here we declare that Groups of type groupType1 are to be rendered using the template tmplGroupType1, and that members of the Group are constrained to the Group element (that is, they cannot be dragged outside of the Group's bounds).

There are a number of flags available to control the drag behaviour of members of a group:

  • droppable True by default - indicates that nodes/groups may be dropped onto the group, either from some other group or from the main canvas.

  • constrain False by default - nodes/groups may be dragged outside of the bounds of the group. When you do drag a node/group outside of the bounds of its parent, what happens next depends on the other flags you have set and where you have dropped it. orphan:true, for instance, will cause the node/group to be removed from its parent group. revert will reinstate the node/group's position inside its parent, unless it was dropped on another group.

  • revert True by default - a node/group dragged outside of its parent group will, unless dropped on another group, revert back to its position inside the group.

  • prune False by default. If true, Nodes dropped outside of the Group (and not dropped onto another Group) are removed from the dataset (not just the Group...the entire dataset).

  • orphan False by default. If true, nodes/groups dropped outside of the group (and not dropped onto another group) are removed from the group (but remain in the dataset). When you set this to true, revert is automatically forced to false.

  • dropOverride False by default. If true, nodes dropped onto other groups first have any rules established by this group applied. For instance, if the groups state prune:true, then the node would be removed from the dataset rather than be dropped onto another group.

  • autoGrow False by default. When autoGrow is switched on a group will grow in size to ensure that its child elements are all contained. Events that can cause a resize event include dropping of a node/group onto the group and also dragging a child element so that it intersects the boundary of a group. autoGrow will allow the size of a group to grow indefinitely unless a maxSize is set.

  • autoShrink False by default. When autoShrink is switched on a group will shrink in size such that the child elements are contained by the group, taking into account any padding that may be set. A shrink event may be instigated by dragging a child element or deletion of a child element. autoShrink will allow the size of a group to shrink indefinitely unless a minSize is set. From 6.10.0 onwards, groups may shrink from their left or top edge as well as from their right or bottom edge. If you do not want this behaviour, set allowShrinkFromOrigin to false.

  • autoSize False by default. When true, switches on both autoGrow and autoShrink.

  • minSize Optional, of type Size. When this is provided, the auto size routine will ensure that the group size never falls below the values provided in this object.

  • maxSize Optional, of type Size. When this is provided, the auto size routine will ensure that the group size never goes above the values provided in this object.

  • elastic A new sizing strategy introduced in 6.10.0. An elastic group is one in which auto sizing is switched on, and the size of the group adapts to the position and size of the child elements it contains. When you drag a child element in an elastic group, the group will grow and shrink such that it always encloses the child elements, taking into account any padding that may be set. Read about elastic groups in full below.

  • allowShrinkFromOrigin When autoShrink, autoSize or elastic is set, a group may at times shrink in size from the left and/or top edge. Set allowShrinkFromOrigin to false to disable this behaviour. You can also invoke this behaviour at drag time by holding down the Ctrl or Meta key (on a mac that's the Command key).

Collapsing/Expanding Groups

You can collapse/expand a group using the collapseGroup and expandGroup methods on the Surface widget. When you collapse a group, any edges from any of the member nodes/groups in the group to nodes/groups outside of the group are relocated to the group's container, and a CSS class is applied to the group's container, indicating the collapsed state. When you subsequently expand the group, the edges are placed back onto their appropriate nodes/groups.

It is important to note that when a group is collapsed, the Toolkit does not hide the member nodes/groups automatically for you. But a CSS class of jtk-group-collapsed is added to the group's container, for you to handle this in your CSS.

The endpoint and anchor to be used in the collapsed state can be specified in the group definition in the view:

toolkit.render({
...
view:{
groups:{
"groupType1":{
templateId:"tmplGroupType1",
endpoint:"Blank",
anchor:"Continuous"
}
}
}
...
});

Any valid Anchor/Endpoint (including custom Endpoints) can be used here.

Magnetizing a collapsed/expanded Group

By default, the Surface widget will run the Magnetizer whenever a group is collapsed or expanded: when a Group is expanded, surrounding elements are adjusted so as to ensure the group does not intersect with any other element. When a group is collapsed, the rest of the elements in the view are gathered in towards the collapsed group. This behaviour can be switched off - see the afterGroupChange flag in the Surface widget magnetizer options

Templating

Specifying the canvas

It is not necessarily the case that you wish to use your entire group template as the parent of the group's members. You can set a data-jtk-group-content attribute on the element that you wish to have acting as the parent for the members:

<script type="jtk" id="tmplMyGroup">
<div class="aGroup">
<h1>{{title}}</h1>
<div data-jtk-group-content="true" class="aGroupInner">
<!-- Child elements go here -->
</div>
</div>
</script>

Whenever a group is resized by the auto sizing code in the surface, the surface looks for an element in the group with this attribute, and if found, this is the element to which the surface applies the change of size. Otherwise the size is applied to the group's main element. Keep this in mind from a CSS perspective: your CSS should allow the size of the content area to mandate the size of its parent. Scroll/auto overflow is not supported inside a group element.

TOP


Drag and Drop Groups

You can drag Groups from a palette onto a Surface using the Drop Manager, with two differences from how you would do this to drag a node:

  • you need to set data-jtk-is-group="true" as an attribute on an element that you wish to drag on to the surface as a group
  • the current GroupFactory will be called to prepare a dataset for your new group, rather than the current NodeFactory.

Here's an example from our Nested Groups starter application (showing both nodes and groups, but we're just going to focus on the groups setup):

<div class="sidebar node-palette">
<div title="Drag Node to canvas" data-node-type="node" class="sidebar-item">
<i class="icon-tablet"></i>Drag Node
</div>
<div title="Drag Group to canvas" data-jtk-is-group="true" data-node-type="group" class="sidebar-item">
<i class="icon-tablet"></i>Drag Group
</div>
</div>

The Toolkit instance is configured with both a nodeFactory and a groupFactory:


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

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

...and the call to configure the drag/drop is no different to that which you'd make just for nodes:


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

new SurfaceDropManager({
surface:renderer,
source:document.querySelector(".node-palette"),
selector:"[data-node-type]",
dataGenerator:function(e) {
return {
type:"default"
};
}
});

TOP


Autosizing Groups

Groups can be configured to automatically resize themselves to encompass the extents of their child nodes/groups, via the autoSize flag on a group definition in a view:

 view:{
groups:{
"groupType1":{
templateId:"tmplGroupTypeOne",
autoSize:true
},
"groupType2":{
templateId:"tmplGroupTypeOne",
autoSize:true,
maxSize:{w:600, h:600}
}
}
}

In this example, both group types are declared to auto size, but groupType2 will grow to a maximum of 600 pixels in each axis.

Autosizing is run after a data load (ie. you call toolkit.load(...)), or when data exists in a Toolkit instance and you call toolkit.render(...).

You can run auto sizing on demand (on all groups):


surface.autoSizeGroups()

or on a single group:


surface.sizeGroupToFit(group:Group)

TOP


Layouts

By default, every group has an Absolute layout assigned to it. If your node data has left/top properties in it, these values will automatically be used to place nodes/groups inside of their parent groups.

Specifying in the view

To specify the layout for a specific type of group, set it in that group's entry in your view:


view:{

...

groups:{
"someGroupType":{
...
layout:{
type:HierarchyLayout.type, // (or just use 'Hierarchy' as a string
options:{
orientation:"vertical"
}
}
}
}

...
}

The format of the layout parameter is identical to the layout parameter in the root of the view.

Layout on demand

You can force a layout in a group like this:

 surface.relayoutGroup(group:string|Group)

This will cause a layout to be run immediately on the given group (which may be passed in as the group object, or just its id)

Ad-hoc group layout

From 6.11.0 onwards you can run an ad-hoc layout on a group ay any time:

 surface.adHocGroupLayout(group:string|Group, layoutParams:LayoutParameters)

This will cause the group's layout to be temporarily swapped out with a layout conforming to the spec you provide in layoutParams, the ad-hoc layout will then be run, and the original group layout reinstated (but without running the original layout again of course!)

Relationship to group size

By default, a layout in a group will cause the auto size routine to be run for the group immediately afterwards.


Elastic groups

Elastic groups are a new feature in version 6.10.0, allowing your users to dynamically resize a group by dragging its child elements around inside of it. To setup a group as elastic:

view:{
groups:{
default:{
elastic:true,
minSize:{ w:250, h:250 } // optional, but it does tend to help aesthetically to have a minSize.
}
}
}

Dragging elements out of an elastic group

To drag a child node out of an elastic group, hold down the Shift key prior to the drag (and hold it for the duration of the drag):

Resizing an elastic group from left/top

By default an elastic group may choose to resize from the left and/or top edge. We call this "shrink from origin", and you can suppress that behaviour via a flag on a group definition:

view:{
groups:{
default:{
elastic:true,
allowShrinkFromOrigin:false,
minSize:{ w:250, h:250 } // optional, but it does tend to help aesthetically to have a minSize.
}
}
}

or on an ad-hoc basis by holding down the meta key (command on Macs) prior to the drag:

Nested elastic groups

From 6.11.0 onwards, elastic groups which are themselves children of another elastic group will relay size changes during dragging to their parent, so that the user can see what changes will be made to all the groups.


Further Reading