DEMOS
DOCS
FEATURES
DOWNLOAD
PURCHASE
CONTACT
BLOG

Templating

Introduction

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.

Rotors 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 Rotors, because Rotors' ability to update previously rendered content means that you can update your UI "automatically" from a change to the data.

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} table-column-primary-key">
    <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",
    ...
  }
}

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({
  container:...,
  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({
  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.


Configuring Connectivity

Note from version 1.2.6 onwards this information applies to Groups as well as Nodes/Ports. Prior to version 1.2.6 this applies only to Nodes and Ports.

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.


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}" transform="rotate(${rotate} ${(width / 2) + strokeWidth} ${(height/2) + strokeWidth})"></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.


Rotors

Rotors 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.

If

There are two if statements in Rotors: 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" selected></input>

Notes

  • you can not use the IF statement inside an attribute expression.
  • an IF statement is executed once and once only. If your dataset changes in such a way that the IF statement would have resolved differently, this will not be detected by Rotors. If you have some content you wish to selectively show/hide, a better approach is to render everything and use CSS along with data- attributes to hide the things you do not wish to see under certain conditions.
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>
For

The r-for tag takes a loop attribute that specifies how many iterations you want. It can be a static value:

<r-for loop="5">
  <div>${$index}</div>
</r-for>

..or it can be a computed value (from the data that is in scope at the time the for loop executes):

<r-for loop="somelist.length">
  <div>${$index}</div>
</r-for>

You might, for instance, have called the template that renders the for loop above with this data:

{
  somelist:[0,1,2,3,4]
}

The current loop index is available as the $index parameter inside the template.

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

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

and this call:

var el = Rotors.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="rotors" 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.

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 Rotors. So you could instead say something like:

 <script type="jtk" id="someTemplate">
     <h3>${title}</h3>
     <r-tmpl lookup="${lookupTemplate(nestedId)}" default="def"/>
 </script>

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>

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

Rotors 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, Rotors 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.


Alternative Template Renderers

You can provide your own template renderer to replace Rotors, 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 Rotors, because Rotors' 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:function(templateId:string, data:object, toolkit:jsPlumbToolkitInstance, objectType:string, surface:Surface) {

        // return a DOM element
    },

    ...

});

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 or port. Useful in certain situations.
  • surface The surface into which the template is being rendered.

Your template renderer is expected to return a single DOM element.