Views
Views are used to define the visual appearance of the various artifacts (which template to use for nodes/groups/ports, CSS classes, connector types and anchors for edges, etc) and event registration. If you want to read about defining connectivity rules, see the Data Model documentation.
When using vanilla Toolkit (no library integrations), views are provided as an argument to the render
method of an instance of the Toolkit. When using a library integration views are specified separately from render parameters.
The general form of a view is that it contains a section for nodes
, edges
, ports
and groups
, the keys of which are types, and the values are sets of configuration. Not every section needs to appear in a view - a view can, in fact, be entirely empty or not present at all, but views are the glue that holds the Toolkit's UI layer together.
Example
Just to jump in quickly, this example is an edited View from the Schema Builder example application.
In this view we have two node types, each of which declares the ID of a template to use to render it, and the "view" node type also declares a click handler. In the edges
section we see three edge types, with the "common" type acting a base type for the others. The ports
section declares a single type - "column" - which indicates the ID of a template to use to render columns, the endpoint/anchor definitions, and also what "type" to set on edges dragged from the port. Note that there is no groups
section in this view; we don't need it for this app.
Below, we'll go through the options for each section in detail.
import { AnchorLocations, LabelOverlay, DotEndpoint, EVENT_CLICK, StateMachineConnector } from "@jsplumbtoolkit/browser-ui"
toolkit.render(someTargetElement, {
view:{
// we have two node types - 'table' and 'view'.
nodes:{
"table":{
// we use 'tmplTable' to render tables
templateId:"tmplTable"
},
"view":{
// and 'tmplView' to render views
templateId:"tmplView",
events:{
// when you click a view Node, we alert its id.
[EVENT_CLICK]:(params:{node:Node}) => {
alert("click on view " + params.node.id);
}
}
}
},
edges:{
// common appearance of all edges
"common":{
connector:StateMachineConnector.type,
paintStyle:{ lineWidth:2, strokeStyle:"#CCC" }
},
// a 1:1 relationship
"1:1":{
parent:"common", // declared 'common' as its parent.
overlays:[
{ type:LabelOverlay.type, options:{ label:"1", location:0.1 } },
{ type:LabelOverlay.type, options:{ label:"1", location:0.9 } }
]
},
"1:N":{
parent:"common",
overlays:[
{ type:LabelOverlay.type, options:{ label:"1", location:0.1 }},
{ type:LabelOverlay.type, options:{ label:"N", location:0.9 }}
]
}
},
ports:{
// a table has an arbitrary number of columns; it is a table's columns that actually connect to other tables, not a table itself.
"column":{
// use 'tmplColumn' to render a Port of type 'column'
templateId:"tmplColumn",
// the appearance of the endpoint on a column
endpoint:{type:DotEndpoint.type, options:{ radius:7 } },
// anchor locations on a column
anchor:[ AnchorLocations.Left, AnchorLocations.Right ],
// the type of edge that will be created from this port by default when the user drags a connection
edgeType:"common"
}
}
}
})
Mapping types
An instance of the Toolkit has the concept of a typeFunction - a function that, when given the data corresponding to some node, port, edge or group, returns the object's type
(as a string). The type returned by this function is what is used as the key into a view (for instance, the "table" and "view" node types above, or the "1:N" edge type etc).
Default Types
For each of nodes, groups, ports and edges, you can provide a default definition, using the key "default"
:
{
...
view:{
nodes:{
"default":{
templateId:"tmplNode"
}
}
},
...
}
This definition will be used in two circumstances:
- no
type
was derived for some object by the current typeFunction - a
type
was derived but there is no entry in the view for that type
For convenience, @jsplumb/common
exports a constant DEFAULT
whose value is "default":
import { DEFAULT } from "@jsplumbtoolkit/browser-ui"
{
...
view:{
nodes:{
[DEFAULT]:{
templateId:"tmplNode"
}
}
},
...
}
Default Edge Type
If you do not specify an edgeType
parameter on a port definition, the type will be set to default
.
Definition Inheritance
Any node, port, edge or group definition can declare a parent
. The resulting definition consists of the parent's entries, with the child's entries merged on top. An example:
{
...
view:{
nodes:{
"common":{
events:{
[EVENT_CLICK]:(params:{node:Node}) => {
console.log("Click on node", params.node);
}
}
},
bigNode:{
parent:"common",
templateId:"tmplBigNode"
},
smallNode:{
parent:"common",
templateId:"tmplSmallNode"
}
}
},
...
}
Here, we have defined a common click handler on the parent definition, and then defined templates for each Node type in their own definitions.
As of 5.11.0, parent
may specify an array of definition ids:
{
...
view:{
nodes:{
"sourceArrow":{
overlays:[ { type:"Arrow", options:{location:0, direction:-1}}]
},
targetArrow:{
overlays:[ { type:"Arrow", options:{location:1}}]
},
bothArrows:{
parent:["sourceArrow", "targetArrow"]
}
}
},
...
}
Common properties
Each entry type in the view is represented by the Toolkit as an interface, and each of these interfaces extends one common interface - ViewOptionsCommon
. The available parameters in the common interface are:
Interface ViewOptionsCommon
Members
connector
, anchor
and anchors
will
always overwrite any such values in the parent. But other values, such as overlays
, will be merged with their
parent's entry for that key. You can force a child's type to override _every_ corresponding value in its parent by
setting mergeStrategy:'override'
.Render events
Each type of object in the View supports declarative registration of events, through the inclusion of an events
member in the appropriate definition. The list of supported events is as follows:
tap
dbltap
click
dblclick
mousedown
mouseup
mouseout
mouseover
contextmenu
view:{
nodes:{
someNodeType:{
events:{
click:(params) => {
// params.toolkit is the Toolkit instance
// params.renderer is the Surface widget
// params.e is the original mouse event
// params.node is the Toolkit Node
// params.el is the element from the DOM representing the Node
}
}
}
},
edges:{
someEdgeType:{
events:{
mouseover:(params) => {
// params.toolkit is the Toolkit instance
// params.renderer is the Surface widget
// params.e is the original mouse event
// params.edge is the Toolkit Edge
// params.connection is a jsPlumb Connection object.
console.dir(params.edge, params.connection);
}
}
}
},
ports:{
somePortType:{
events:{
mousedown:(params) => {
// params.toolkit is the Toolkit instance
// params.renderer is the Surface widget
// params.e is the original mouse event
// params.port is the Toolkit Port
// params.endpoint is a jsPlumb Endpoint object
console.dir(params.port, params.endpoint);
}
}
}
},
groups:{
someGroupType:{
events:{
click:(params) => {
// params.toolkit is the Toolkit instance
// params.renderer is the Surface widget
// params.e is the original mouse event
// params.group is the Toolkit group
// params.el is the element from the DOM representing the group
}
}
}
}
}
Nodes
The main thing you'll want to map in a node definition is the template to use to render a node of that type. You can do this with either the templateId
property, which identifies a template by its ID that you are expecting the Toolkit to be able to resolve from the DOM, or with the template
property, where you provide the template inline.
Entries in the nodes
section are expected to conform to the ViewNodeOptions
interface, which is itself inherited from ViewNodeOrPortOptions
and in fact currently adds no extra keys. So here we show the properties available on ViewNodeOrPortOptions
Interface ViewNodeOrPortOptions
Members
allowVertexLoopback
.ArrayAnchorSpec
,
meaning an array in the format [ x, y, orientationX, orientationY, offsetX, offsetY ]. Note also that offsetX
and offsetY
are optional,
and will be defaulted to 0.connector
, anchor
and anchors
will
always overwrite any such values in the parent. But other values, such as overlays
, will be merged with their
parent's entry for that key. You can force a child's type to override _every_ corresponding value in its parent by
setting mergeStrategy:'override'
.templateId
, this will take precedence.template
in that when you provide templateId
you are expecting the Toolkit to resolve
the template for you, either from a templates
block in a render
call, or by looking for a script element in the DOM with the appropriate ID. If you
provide this and also template
, template
will take precedence.Edges
Edges in a view conform to the ViewEdgeOptions
interface, which offers an extensive means of configuring the appearance and behaviour of your edges.
Interface ViewEdgeOptions
Members
label
will be used
as the overlay's label. This value can be parameterised, in order to extract a value from the edge's backing data, eg. if you set label:"{{name}}"
then the Toolkit would attempt to extract a property with key name
from the edge's backing data, and use that property's value as
the label.label
is set. This is a static string and does not support
parameterisation like label
does.labelLocationAttribute
.labelLocation
, and indicates the name of a property whose value can be expected to hold the location at
which the label overlay should be located. The default value for this is labelLocation
, but the key here is that the Toolkit
looks in the edge data for labelLocation
, so the location is dynamic, and can be changed by updating labelLocation
.
This parameter allows you to change the name of the property that the Toolkit will look for in the edge data.connector
, anchor
and anchors
will
always overwrite any such values in the parent. But other values, such as overlays
, will be merged with their
parent's entry for that key. You can force a child's type to override _every_ corresponding value in its parent by
setting mergeStrategy:'override'
.Edge labels
Since 5.11.0 it has been possible to use a shortcut syntax to display a label on each edge. See the label
option listed above? You can use this have the Toolkit automatically create a label overlay for some edge type:
edges:{
"someType":{
label:"FOO"
}
}
Here, we've said that all edges of type "someType" will have a label with the text "FOO". We can also use dynamic values:
edges:{
"someType":{
label:"{{labelKey}}"
}
}
In this example, the Toolkit will extract the value of the property labelKey
from each edge's backing data, and set that as the text in the label overlay.
Edge label location
By default, a label overlay created with this mechanism will be located at 0.5, that is halfway along the length of the edge. When you provide label
in an edge definition, the Toolkit also registers an observer for the property with key labelLocation
, and if a value for this key is present, the location of the label will be placed accordingly. So with this edge data and the above label:"{{labelKey}}"
mapping:
{
labelKey:"i am the label",
labelLocation:0.2
}
You'd get a label overlay with "i am the label" positioned at 0.2.
You can change the name of the attribute the Toolkit uses to determine label location if you wish:
edges:{
"someType":{
label:"{{labelKey}}",
labelLocationAttribute:"whereTheLabelShouldBe"
}
}
We'd use this edge data in that case:
{
labelKey:"i am the label",
whereTheLabelShouldBe:0.2
}
Ports
Ports are vertices in the data model, and can be rendered either as a DOM element, or as an endpoint. Above, we saw the ViewNodeOrPortOptions
interface, which defines common properties between nodes and ports. We'll repeat it here to save you scrolling:
These are the options that are also available on a port definition:
As with nodes, ports can be associated with a template for rendering. They do no have to be - in some cases, you will want to use an endpoint to represent a port, and in others you'll want to use a DOM element. If you're using DOM elements and your application supports the dynamic addition of new ports - as in the case of the Schema Builder when the user adds a new column to a table - then you need to provide the Toolkit with a separate template to use to render the port (as opposed to rendering the ports inside of each node's template).
Groups
For a full discussion on Groups, including how to map them inside Views, take a look at this page.
Preconfigured Parameters
You may have a template that you'd like to reuse for a collection of nodes, with slight variations between each. Consider this example:
<script type="jtk" id="tmplRectangle-svg">
<svg:svg style="position:absolute;left:0;top:0;" version="1.1" xmlns="http://www.w3.org/1999/xhtml">
<svg:rect width="{{width}}" height="{{height}}" x="{{strokeWidth}}" y="{{strokeWidth}}" fill="{{fill}}" stroke="{{stroke}}" stroke-width="{{strokeWidth}}"/>
</svg:svg>
</script>
This template draws SVG rectangles, of any given size, with arbitrary fill and stroke, and possibly rotated. Let's say we're creating an application in which there are two types of rectangle shapes: big red ones, and little yellow ones. In our View we can define custom parameters for each of these node types, which will be mixed in with the node's data when it comes time to render:
view:{
nodes:{
"bigRed":{
templateId:"tmplRectangle",
parameters:{
width:250,
height:250,
fill:"red"
}
},
"smallYellow":{
templateId:"tmplRectangle",
parameters:{
width:50,
height:50,
fill:"yellow"
}
}
}
}
Preconfigured parameters are known to break 2-way data binding when using the Toolkit with AngularJS, because the data object passed to the template is a copy of the original. To switch off preconfigured parameters, set enhancedview:false
in your render
call. With the other library integrations - Angular, Vue 2/3, Svelte and React - the Toolkit takes care of setting this flag appropriately.
Preconfigured Parameters & Inheritance
Preconfigured parameters are merged when one definition inherits from another, but only to the first level. Consider this arrangement:
view:{
nodes:{
"base":{
templateId:"tmplRectangle",
parameters:{
lineWidth:2,
foo:{
bar:"baz"
}
}
},
"bigRed":{
parent:"base",
parameters:{
width:250,
height:250,
fill:"red",
foo:{
qux:"FOO"
}
}
},
"smallYellow":{
parent:"base",
parameters:{
width:50,
height:50,
fill:"yellow"
}
}
}
}
Note the foo
parameter is declared in all three definitions. After merging, the two concrete node definitions have these values:
"bigRed":{
parent:"base",
parameters:{
width:250,
height:250,
fill:"red",
foo:{
qux:"baz"
}
}
}
"smallYellow":{
parent:"base",
parameters:{
width:50,
height:50,
fill:"yellow",
foo:{
bar:"baz"
}
}
}
So the foo
entry in bigRed would completely overwrite the foo
entry from base; the two are not merged together. smallYellow does not have foo
entry and therefore inherits it from base.
Preconfigured parameters operate at the view level only: they are not written into your data model. So you cannot use this mechanism to provide parameters that you will subsequently update (such as the w
/h
parameters that the Flowchart Builder demo uses: if provided via the preconfigured parameters mechanism the UI would initially render correctly, but if the w
/h
values were updated, the template would not re-render. In the Flowchart Builder, the solution is to provide the w
/h
in the data model).
Switching off Preconfigured Parameters
As mentioned above, there are some cases in which you cannot use preconfigured parameters. One such known case is when you are using AngularJS and you're taking advantage of the two-way data binding provided by its template engine. Preconfigured parameters (and function parameters, discussed below), cause a copy of the original data to be created, which then breaks AngularJS's two-way binding. A copy is created because the only other option is to copy the preconfigured parameters into the original data, which is almost certainly not what you want, given that they are a view concern.
To switch off preconfigured parameters and function parameters, set the enhancedView
flag on your render
call to false
:
_toolkit.render({
container:"someElement",
view:{
nodes:{
"circleNodeDef" : {
templateId:"tmplCircle",
parameters:{
lineWidth:5,
radius:10,
fill:"black"
}
}
}
},
enhancedView:false
});
Function Parameters
By default you can provide values in definitions as functions. These will be given the backing data for the object being rendered. An example definition:
_toolkit.render(document.getElementById("someElement"), {
view:{
nodes:{
"circleNodeDef" : {
templateId:"tmplCircle",
parameters:{
lineWidth:5,
radius:10,
fill:"black",
stroke:function(data) { return data.error ? "red" : "green"; }
}
}
}
}
});
If we were to make this call:
var node = _toolkit.addNode({
type:"circleNodeDef",
error:true
});
we'd end up with a circle that has a red outline. Otherwise we'd get a green outline.
Function parameters are known to break 2-way data binding when using the Toolkit with AngularJS, because the data object passed to the template is a copy of the original. To switch off function parameters, set enhancedView:false
in your render
call.
Switching off Function Parameters
See Switching off Preconfigured Parameters.
View Options
The specific interface for a view now is SurfaceViewOptions