- Introduction
- Limitations
- Inferring Template IDs
- Custom Template Resolver
- Providing Templates Directly
- Configuring Connectivity
- External Templates
- Knockle
- Rendering SVG
- Custom Template Renderer
Templating
One great feature of the jsPlumb Toolkit is the way client side templating is baked right in - your Node, Group and Port definitions in the View declare the id of some template to use and the Toolkit takes care of the rest.
Knockle is the default template adapter in the jsPlumb Toolkit. You can use any templating mechanism you want to - you just need to write an appropriate adapter, and provide this function as the value of the templateRenderer
parameter - but it is recommended that you use Knockle, because Knockle's ability to update previously rendered content means that you can update your UI "automatically" from a change to the data.
Note : if you are using the Angular, Vue or React integration, then Knockle is not used for your templates. You'll be writing components for that library and using its own template syntax.
Here's what the table
template looks like in the Database Visualizer application:
<script type="jtk" id="tmplTable">
<div class="table">
<div class="table-name">
${name}
<div class="new-column" title="Click to add a new column">+</div>
</div>
<ul class="table-columns">
<r-each in="columns">
<r-tmpl id="tmplColumn"></r-tmpl>
</r-each>
</ul>
</div>
</script>
and here's tmplColumn
:
<script type="jtk" id="tmplColumn">
<li class="table-column table-column-type-${type} {{if primaryKey}}table-column-primary-key{{/if}}">
<div class="table-column-delete" data-port-id="${id}">x</div>
${id}
<jtk-source port-id="${id}" port-type="column" scope="${datatype}"></jtk-source>
<jtk-target port-id="${id}" port-type="column" scope="${datatype}"></jtk-target>
</li>
</script>
Notice any non-standard HTML in there? The jsPlumb Toolkit supports declarative source/target/endpoint configuration, discussed in Connectivity below.
These templates are matched to Node/Port types in the Database Visualizer's view
. Here's an edited snippet of the view:
nodes: {
"table": {
template: "tmplTable"
}
...
},
...
ports: {
"default": {
template: "tmplColumn",
...
}
}
When using the default template engine, your templates must return a single root node. If you return multiple nodes, the Toolkit will use the first node only.
As shown above, the Node and Port definitions passed as part of the view
parameter on a render
call allow you to specify a template
to be used to render an instance of the given type. Sometimes you will find that you have nothing other than a template
directive in a Node or Port definition. So to cut down on boilerplate, the Toolkit will make a guess at a suitable template id if you do not provide.
The inferred template id consists of "jtk-template-"
as a prefix, followed by the type of the object. So in the previous example, a Node of type table
would have an inferred template id of jtk-template-table
.
The default template resolution mechanism is to look for a <script>
element in the DOM with an ID matching the ID of the template to retrieve. You can supply your own template resolver like this:
toolkit.render({
container:...,
view:{ ... },
templateResolver:function(templateId) {
// find the matching template and return it - as a String.
}
});
Another option is to pass a map of templates into the render
call:
toolkit.render({
container:...,
view:{ ... },
templates:{
type1:"<div><span>type 1</span>...</div>",
type2:"<div><span>type 2</span>...</div>",
}
});
For some use cases, where the templates are not overly complex, this can be handy. One caveat to note with this approach is that Knockle expects double quotes for attributes, so if your template has any attributes declared you need to use escaped double quotes:
toolkit.render({
container:...,
view:{ ... },
templates:{
type1:"<div class=\"foo\"><span>type 1</span>...</div>",
type2:"<div><span>type 2</span>...</div>",
}
});
NOTE This section applies both to the case that you are using Knockle for your templates, or you are using one of the library integrations. One thing to watch out for, though, is that when using Vue you need to instruct Vue to ignore these elements via the inclusion of a v-pre
attribute, for example:
jtk-
Elements
To configure your UI to allow for the user to establish connections using the mouse, you use one or more custom tags in your HTML templates. In the previous section, we saw a template that had a jtk-source
and a jtk-target
element; these were used to declare that the given element should act as both a connection source and connection target.
A third custom element available in the jsPlumb Toolkit is jtk-port
- used to declare an Endpoint on some element.
A full discussion of each element follows.
jtk-source
This element is used when you want to configure some element as a connection source. It is analogous (and maps directly to) the makeSource
method in jsPlumb. The element that is configured as the connection source is that element which is the parent of the jtk-source
element - here, it's the .table-column
li.
Six attributes are supported on the jtk-source
element:
- port-id
This defines the id of the port on the node. It is not mandatory: if you use the jtk-source
element to turn some Node into a connection source, then you don't need to set this attribute - you are configuring the Node's default Port. If you wish to assign this connection source to a Port, though, then you must set this attribute. This attribute's value must be unique on its Node, but may be the same as the id of some Port on another Node. Here we are using the id
member of the data backing the column.
- port-type
Maps to a Port type in the view, as discussed above. Note that you can set this even if you don't set port-id
, as you will want to associate a Node's default Port with some type.
- scope
The jsPlumb Toolkit can make use of jsPlumb's scope
concept to control, in a relatively crude way, what can be connected to what. In this template you see that both the jtk-source
and jtk-target
elements use the same value for scope, and in the Database Visualizer this is mapped to the column's underlying datatype. There is a more sophisticated mechanism available to control connections should you need it: see Interceptors.
- endpoint
Setting this to true
causes jsPlumb to create an Endpoint for the given Node, and not to configure the element itself as a drag source. The created Endpoint is used for any Edges whose source is the given Node. New Edges may be dragged from the Endpoint.
- filter
Oftentimes you will want to configure an element as a connection source but not actually have the entire element respond to a connection drag start. This is quite common in the sorts of UIs for which the jsPlumb Toolkit is used: you need to be able to drag your nodes around the screen, but you'd also like to be able to drag connections from the nodes too. Using filter
you provide a CSS selector that identifies elements from which connection drag should be supported.
- filter-exclude
If you run into a problem specifying a suitable selector for the filter
(:not
selectors, in particular, are a little restricting), you can set filter-exclude:true
, which will mean that the elements identified by the filter
selector will be excluded from starting a drag.
jtk-target
This element is the opposite of the jtk-source
element: it allows you to identify its parent as an element that you wish to configure as a connection target. It also has an analogue in jsPlumb to which it is directly mapped: makeTarget
.
This element supports four of the six attributes supported by jtk-source
. Their meaning and usage is the same:
- port-id
- port-type
- scope
- endpoint
The fifth and six parameters - filter
and filter-negate
- are not supported, which means that it is always the entire element that is configured as a connection target. It is possible that, in the future, jsPlumb will support the filter
and filter-negate
attributes on the makeTarget
method. If/when that happens, the Toolkit's jtk-target
element will be updated to also support them.
jtk-port
Use this element when you want to have an Endpoint added to your UI and map it to a Port. Its analogue in jsPlumb is the addEndpoint
method. This element supports the same attributes as makeTarget
(with the exception of endpoint
), plus some extras:
port-id
port-type
scope
anchor-x This attribute can be used to specify the location, in the x axis, of the Anchor used by the Endpoint associated with this Port. This is a proportional value, as discussed in the anchor documentation.
anchor-y This attribute can be used to specify the location, in the y axis, of the Anchor.
orientation-x Used to associate an orientation, in the x axis, with the Anchor associated with the Port. Orientation is discussed in the anchor documentation linked above. This is optional, but if omitted, the default of 0 will be used.
orientation-y Used to associate an orientation, in the y axis, with the Anchor associated with the Port.
Note using this declarative means of configuring ports does not mean that the UI artefacts associated with the Endpoints exist in the DOM as children of the port's parent element. Remember that the Toolkit always makes use of jsPlumb's Container concept, to ensure that all of the Endpoints and Connections contained inside a given Surface have the same parent.
Note also that any jsPlumb Toolkit elements that are found during rendering are removed from the DOM after processing.
To help you organize your templates better, you can store them in external files and reference them via a script
import in your HTML:
The key here is that the type
is text/x-jtk-templates. There may be one or more templates declared in the specified file. This example is one from one of the Toolkit's unit tests; the file contains several templates. Here's the first two:
<script type="jtk" id="tmplRectangle">
<svg:svg style="position:absolute;left:0;top:0;">
<svg:rect width="${width}" height="${height}" x="${strokeWidth}" y="${strokeWidth}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}" {{if rotate}}transform="rotate(${rotate} ${(width / 2) + strokeWidth} ${(height/2) + strokeWidth})"{{/if}}></svg:rect>
</svg:svg>
</script>
<script type="jtk" id="tmplCircle">
<svg:svg style="position:absolute;left:0;top:0;">
<svg:circle class="jtk-svg" cx="${cx}" cy="${cy}" r="${r}" fill="${fill}" stroke="${stroke}" stroke-width="${strokeWidth}"></svg:circle>
<jtk-port port-id="port" port-type="basic"></jtk-port>
</svg:svg>
</script>
You can have multiple x-jtk-templates
scripts declared in your html. Note, though, that the loading of these scripts postpones the firing of the ready
event in the Toolkit, so you should ensure you're not getting any 404s and what you are loading is loading quickly.
Knockle is the default templating mechanism used by the jsPlumb Toolkit. It uses a strict XHTML syntax and can run both in the browser and headless on the server.
These are the key points:
- Format is strict XHTML: all tags must be closed. This means
for example. The only exception to this rule is the <r-else>
element:
- Use only double quotes for attributes:
<div class="foo"></div>
not
<div class='foo'></div>
Inside attribute values, however, you can use single quotes:
<r-if test="value == 'foo'">...</r-if>
Tags
Each
With Objects in an Array
With Arrays in an Array
The key here is that the current array is exposed as the variable $data
.
With an Object
The key here is that each entry is presented to the template as an object with $key
and $value
members.
Uniquely identifying child nodes
In some cases, such as the Database Visualizer application that ships with the Toolkit, you might want to use the r-each
element to loop through your data and add ports to your nodes. For instance:
This is how the database visualizer application renders the columns in a table. tmplColumn
looks kind of like this:
<script type="jtk" id="tmplColumn">
<li class="table-column table-column-type-${datatype}" primary-key="${primaryKey}" data-port-id="${id}">
...
<jtk-source port-id="${id}" port-type="column" scope="${datatype}" filter=".table-column-delete, .table-column-delete-icon, span, .table-column-edit, .table-column-edit-icon" filter-exclude="true"></jtk-source>
<jtk-target port-id="${id}" port-type="column" scope="${datatype}"></jtk-target>
</li>
</script>
with some details removed for brevity. The thing to note is that this template, which is looped over, generates ports for the node. Note the key
attribute on the r-each
element above. It instructs Knockle on how to get a unique identifier for each value in the loop. Thus, if you change the data in some port, Knockle will just update the port's element, since it can use the key
property to identify the existing element. Similarly, if you add a new port, Knockle will use the key
to determine that it has no current element, and if you remove a port, Knockle will use the key
to determine that the element corresponding to that port is no longer needed, and will remove it.
Knockle will log a message to the console any time the r-each
element is used without a key
. It isn't an error, but in many cases it's likely you'll want to provide the key
for each child.
If
There are two if
statements in Knockle: one that is an element, which you use in the body of your templates, and one that is inline, which you use inside tags to selectively include/exclude attributes:
Existence
<r-if test="someObjectRef">
<div>hola</div>
</r-if>
Expressions
Inline
Notes
- you can not use the IF statement inside an attribute expression.
Else
The element version of the IF statement has an optional ELSE statement:
<r-if test="something">
<div class="success">ok</div>
<r-else>
<div class="fail">things are not ok.</div>
</r-if>
Comments
Comments follow the standard XHTML syntax:
Comments are stored in the parse tree for a template. This may or may not prove useful.
Embedding HTML
By default Knockle treats text as plain text. For example with this template:
and this call:
The innerHTML of the span
would be the string "<h1>Hello</h1>"
.
You can use the r-html
tag to indicate that you're expecting HTML:
Now you'll get a span
with an h1
child element:
Nested Templates
With specific context
Inheriting parent context
The difference between these two examples is that in the first, an item called someItem
is extracted from the current dataset, and passed in to the nested
template, whereas in the second, the nested
template is passed the exact same data that the parent is currently using to render itself.
Inside an r-each loop
This is similar to the example immediately above - the nested template inherits its parent's context, but in this case the parent's context is currently some item from the list. You can also use context
in this situation:
<div>
<r-each in="someList">
<r-tmpl id="nested" context="someMemberOfTheListItem"></r-tmpl>
</r-each>
</div>
The context for the nested element here is the someMemberOfTheListItem
member of each list item.
With complex context
You are not limited to extracting single variables from the current context to pass in to a nested template. You can specify a complex object too:
In this example, foo
will be extracted from the context in which the current template is executing, and Hello
is a hardcoded string.
####### Accessing nested properties
You can also specify properties that are nested inside the current context, either with dotted notation:
or by naming the property:
Dynamic Template Names
You can lookup the name of a nested template at runtime, for example consider these templates:
<script type="jtk" id="someTemplate">
<h3>${title}</h3>
<r-tmpl lookup="${nestedId}" default="def"/>
</script>
<script type="jtk" id="green">
<h3>GREEN</h3>
</script>
Here we see the ID of the nested template is derived from the nestedId
property of the data we are rendering:
default
allows you to provide the ID of a template to use if the lookup fails.
Note that with lookup
you can use arbitrary Javascript, as you can elsewhere in Knockle. So you could instead say something like:
<script type="jtk" id="someTemplate">
<h3>${title}</h3>
<r-tmpl lookup="${lookupTemplate(nestedId)}" default="def"/>
</script>
To render SVG elements you must prefix the tag with a namespace:
<svg:svg width="50" height="50">
<svg:rect x="10" y="10" width="10" height="10"></svg:rect>
</svg:svg>
Updating Data
Given this template for some node type:
<div class="someNode">
<span>${title}</span>
<ul>
<r-each in="someDataMember">
<li>${id}</li>
</r-each>
</ul>
</div>
and this call to a Toolkit instance:
var node = toolkit.addNode({
title:"FOO",
someDataMember:[
{ id:"one" },
{ id:"two" },
{ id:"three" }
]
});
You'll get a node element with a span
that says FOO
, and a list of three items: one
, two
and three
.
Calling updateNode
on the Toolkit instance associated with this node:
toolkit.updateNode(node, {
title:"FOO-NEW",
someDataMember:[
{ id:"un" },
{ id:"deux" },
{ id:"trois" }
]
});
will result in a node element with a span
that says FOO-NEW
, and a list of three items: un
, deux
and trois
.
Updating the class
attribute
Knockle won't update the class attribute once a template has been written. Since the update
method can only write values for classes that were in the template, there's a risk that any classes added by other parts of your app would be removed. For example say you have this template:
If you render this with {nodeType:"start-node"}
then you'd end up with a div
with class start-node
. Then say some code comes along and does this:
Now you've got a div
with class start-node selected
. If you then called update
, Knockle would re-write the class attribute to have only the nodeType
class; probably not at all what you want. In this scenario you are better off using attribute selectors.
You can provide your own template renderer to replace Knockle, should you wish to, by supplying it as the templateRenderer
argument to a render
call on an instance of the Toolkit.
Note as mentioned above, it is recommended that you use Knockle, because Knockle's ability to update previously rendered content means that you can update your UI "automatically" from a change to the data. Using some other template renderer will mean that the updateNode
, updatePort
and updateGroup
methods cannot automatically update your UI.
var surface = toolkitInstance.render({
...
templateRenderer:{
render: function(templateId:string, data:object, toolkit:jsPlumbToolkitInstance, objectType:string, surface:Surface) {
// return a DOM element
},
cleanup: function(objectId, element) {
// do any cleanup required, remove the element from the dom.
}
},
...
});
templateRenderer
is required to supply two functions:
render
The method parameters are:
- templateId The ID of the template to render. This will have been derived from the mapping of the node/group/port type in your view.
- data The backing data for the Node/Group/Port being rendered - this is the data you have passed in to the Toolkit.
- toolkit The associated Toolkit instance.
- objectType One of
node
,group
orport
. Useful in certain situations. - surface The surface into which the template is being rendered.
This method is expected to return a single DOM element.
cleanup
- objectId The id of the object being removed
- element The DOM element for the object being removed. This is the same object you returned from
render
.