Templating
This page discusses how to write templates for JsPlumb's internal template engine. If you're using Vanilla JsPlumb then you'll be writing templates in this format for your nodes and groups; if you're using a library integration then you'll be using the component syntax for that library instead.
Templates in the format discussed on this page are also used by the Dialogs and Inspector components, as well as in Shape sets - regardless of whether you are using a library integration or not.
Template format
- Format is strict XHTML: all tags must be closed. This means
<input type="text"></input>
for example.
- 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>
Limitations
Your templates must return a single root node. If you return multiple nodes, JsPlumb will use the first node only.
Interpolating values
To extract some value from a node/group that a given template is rendering, use this syntax:
<h1>{{name}}</h1>
So for some node with this dataset:
{
name:"My Node"
}
You'd get this output:
<h1>My Node</h1>
Values within attributes
The template engine will extract values from the dataset within attributes, with some limitations. Let's enhance the heading example from above with a title attribute:
<h1 aria-label="{{title}}" title="{{title}}">{{name}}</h1>
So for some node with this dataset:
{
name:"My Node",
title:"This is the name of the node"
}
You'd get this output:
<h1 aria-label="This is the name of the node" title="This is the name of the node">My Node</h1>
Limitations
You can use basic math inside an attribute value, for instance:
<div>
<svg:svg width="{{width}}" height="{{height}}">
<svg:circle cx="{{width / 2}}" cy="{{height / 2}}" rx="{{width / 2}}" ry="{{height / 2}}"
</svg:svg>
</div>
These expressions can include parentheses:
<div>
<svg:svg width="{{width}}" height="{{height}}">
<svg:circle cx="{{(width-10) / 2}}" cy="{{(height-10) / 2}}" rx="{{(width-10) / 2}}" ry="{{(height-10) / 2}}"
</svg:svg>
</div>
what you cannot do, though, is eval arbitrary Javascript:
<div>
<svg:svg width="{{width}}" height="{{height}}">
<svg:circle cx="{{calcCenterX(width)}}" cy="{{calcCenterHeight(height)}}" rx="{{calcRx(width)}}" ry="{{calcRy(height)}}"
</svg:svg>
</div>
This will not work. You can use macros to insert computed values.
Rendering SVG
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>
This is due to the fact that the templating code uses createElementNS
to create elements.
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 JsPlumb 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 JsPlumb 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
Recado 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:
<div class="{{nodeType}}">
FOO
</div>
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:
myDiv.classList.add("selected");
Now you've got a div
with class start-node selected
. If you then called update
, JsPlumb 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.
Template Macros
These are a means for you to inject values into your templates that require computation at runtime. You indicate to the template engine that you wish to invoke a macro by prefixing the value to interpolate with a hash. A slightly more convoluted version of the above example:
<script type="jtk" id="tmplTable">
<div data-id="{{id}}">
<h1>{{#truncatedId}}</h1>
<p>{{content}}</p>
<p>{{#concatTags}}</p>
</div>
</script>
We use two macros here:
toolkit.render(someElement, {
templateMacros:{
truncatedId:(data) => data.id.substring(0, 5),
concatTags:(data) => data.tags.join(" ")
}
})
The data
argument passed in to each macro is the vertex's backing data. For example, for this setup, we might have this payload:
{
"id": "78947329843h2hjlkshkfasd789",
"content": "Loretta ipsum",
"tags": [ "foo", "bar", "qux"]
}
Tag reference
Each
With objects in an array
{
someDataMember:[
{ id:"one", label:"value1" },
{ id:"two", label:"value2" }
]
<ul>
<r-each in="someDataMember">
<li id="{{id}}">{{label}}</li>
</r-each>
</ul>
With Arrays in an Array
{
someDataMember:[
[ "one", "value1" ],
[ "two", "value2" ]
]
<ul>
<r-each in="someDataMember">
<li id="{{$value[0]}}">{{$value[1]}}</li>
</r-each>
</ul>
The point to note here is that the current array is exposed as the variable $value
.
With an Object
{
someData : {
id:"foo",
label:"FOO is the label",
active:true,
count:14
}
<table>
<r-each in="someData">
<tr><td>{{$key}}</td><td>{{$value}}</td></tr>
</r-each>
</table>
The point to note 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 Schema Builder application that ships with JsPlumb, you might want to use the r-each
element to loop through your data and add ports to your nodes. For instance:
<ul class="table-columns">
<r-each in="columns" key="id">
<r-tmpl id="tmplColumn"/>
</r-each>
</ul>
This is how the schema builder 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}}">
...
</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 Recado on how to get a unique identifier for each value in the loop. Thus, if you change the data in some port, Recado 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, Recado will use the key
to determine that it has no current element, and if you remove a port, Recado will use the key
to determine that the element corresponding to that port is no longer needed, and will remove it.
JsPlumb will log a message to the console any time the r-each
element is used without a key
. If you do not supply a key
then JsPlumb will not be able to perform an update of the loop.
If
Inline {{if ...}}
statements in attributes are not supported.
Existence
<r-if test="someObjectRef">
<div>hola</div>
</r-if>
An existence test will be evaluated according to Javascript's "falsy" rules. If you are unfamiliar with falsiness in Javascript, you might like to take a look here
Expressions
<r-if test="foo == 5">
<div>hola</div>
</r-if>
Expressions are limited by the following rules:
- the only comparators supported are
==
,===
,<=
,<
,>
,>=
- javascript expressions are not supported (eg
someMethod(foo) == 5
)
Comments
Comments follow the standard XHTML syntax:
<div>
<!--
a comment
<span>Maybe some code was commented</span>
-->
</div>
Comments are stored in the parse tree for a template. This may or may not prove useful.
Embedding HTML
By default Recado treats text as plain text. Unlike its predecessor, Recado does not support embedding arbitrary HTML.
Nested Templates
With specific context
<div>
<r-tmpl id="nested" context="someItem"></r-tmpl>
</div>
Inheriting parent context
<div>
<r-tmpl id="nested"></r-tmpl>
</div>
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
<div>
<r-each in="someList">
<r-tmpl id="nested"></r-tmpl>
</r-each>
</div>
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:
<div>
<r-tmpl id="nested" context="{id:foo, label:'Hello'}"/>
</div>
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:
<div>
<r-tmpl id="nested" context="{id:record.id, label:'Hello'}"/>
</div>
or by naming the property:
<div>
<r-tmpl id="nested" context="{id:record['id'], label:'Hello'}"/>
</div>
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:
{
title:"example",
nestedId:"green"
}
default
allows you to provide the ID of a template to use if the lookup fails.