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.
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"
}
}
}
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}`)
}
}
}
}
}
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 tofalse
.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 amaxSize
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 anypadding
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 aminSize
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, setallowShrinkFromOrigin
to false.autoSize False by default. When true, switches on both
autoGrow
andautoShrink
.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 anypadding
that may be set. Read about elastic groups in full below.allowShrinkFromOrigin When
autoShrink
,autoSize
orelastic
is set, a group may at times shrink in size from the left and/or top edge. SetallowShrinkFromOrigin
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.
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 currentNodeFactory
.
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"
};
}
});
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)
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
- The views documentation provides a full list of configuration you can use with node and group mappings
- Shape libraries provide a way to inject SVG into your nodes depending on their type. We use shape libraries for many of our diagramming apps such as the Flowchart Builder starter app.
- The Angular documentation contains information about how to map nodes with Angular
- The React documentation contains information about how to map nodes with React
- The Vue 2 documentation contains information about how to map nodes with Vue 2
- The Vue 3 documentation contains information about how to map nodes with Vue 3
- The Svelte documentation contains information about how to map nodes with Svelte