Skip to main content

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. Ff 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 node">        <div class="name">            <div class="delete" title="Click to delete"/>            <span>${name}</span>            <div class="buttons">                <div class="edit-name node-edit" title="Click to edit table name"/>                <div class="new-column add" title="Click to add a new column"/>            </div>        </div>        <div class="table-columns">            <r-each in="columns" key="id">                <r-tmpl id="tmplColumn"/>            </r-each>        </div>    </div></script>

and here's tmplColumn:

<script type="jtk" id="tmplColumn">    <div class="table-column table-column-type-${datatype}" primary-key="${primaryKey}" data-jtk-port="${id}" data-jtk-scope="${datatype}" data-jtk-source="true" data-jtk-target="true">
        <div class="table-column-delete">            <i class="fa fa-times table-column-delete-icon"/>        </div>        <div><span>${name}</span></div>        <div class="table-column-edit">            <i class="fa fa-pencil table-column-edit-icon"/>        </div>    </div></script>

Notice any non-standard HTML in there? The 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": {    templateId: "tmplTable"  }  ...},...ports: {  "default": {    templateId: "tmplColumn",    ...  }}

Limitations#

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.

Inferring Template IDs#

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.

Custom Template Resolver#

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(someElement, {  view:{ ... },  templateResolver:function(templateId) {    // find the matching template and return it - as a String.  }});

Providing Templates Directly#

Another option is to pass a map of templates into the render call:

toolkit.render(someElement, {  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(someElement, {  view:{ ... },  templates:{    type1:"<div class=\"foo\"><span>type 1</span>...</div>",    type2:"<div><span>type 2</span>...</div>",  }});

External Templates#

To help you organize your templates better, you can store them in external files and reference them via a script import in your HTML:

<script type="text/x-jtk-templates" src="../palettes/shapes-svg.html"></script>

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-endpoint port-id="port" port-type="basic"></jtk-endpoint>  </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#

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
<input type="text"></input>

for example. The only exception to this rule is the <r-else> element:

<r-if test="size > 10">   <h1>large</h1><r-else>   <h6>small</h6></r-if>
  • 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#
{  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="${$data[0]}">${$data[1]}</li>    </r-each></ul>    

The key here is that the current array is exposed as the variable $data.

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 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:

<ul class="table-columns">    <r-each in="columns" key="id">        <r-tmpl id="tmplColumn"/>    </r-each></ul>

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}">        ...             </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#
<r-if test="foo == 5">    <div>hola</div></r-if>
Inline#
<input type="radio" class="foo"{{if selected}} selected{{/if}}></input>

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:

<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 Knockle treats text as plain text. For example with this template:

<script type="jtk" id="tmplExample">     <div>        <span>${text}</span>    </div></script>

and this call:

var el = Knockle.template("tmplExample", { text:"<h1>Hello</h1>" });

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:

<script type="jtk" id="tmplExample">     <div>        <span><r-html>${text}</r-html></span>    </div></script>

Now you'll get a span with an h1 child element:

<div>    <span>        <h1>Hello</h1>    </span></div>

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.

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:

<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).addClass("selected");

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.