YATL
JavaScript HTML CSS

README.md

Rotors

Rotors is the default templating mechanism used by the jsPlumb Toolkit. It began life as a port of an early version of jQuery templates, from which the jQuery dependency was removed. Then various changes were made, not the least of which was the addition of support for updating rendered templates, and a few method names were changed and some syntax revised. At that point, in the core of Rotors you could still find the original jQuery templates code, but since that time a complete, ground-up rewrite has occurred, and Rotors now has no traces of jQuery templates at all.

Rotors uses a strict XHTML syntax and can run both in the browser and headless on the server.

Quick Start Guide

These are the key points:

  • You need to get an instance of Rotors before you do anything. All of the examples in this guide will assume you first ran this line of code:
var rotors = Rotors.newInstance();
  • 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. (I know I said all tags. I just wanted to impress upon you the importance of thinking about strict XHTML before admitting there was an exception).

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

Rendering

var rotors = Rotors.newInstance();
var fragment = rotors.template("someId", { data });

Here, fragment is, when running in a browser, a DocumentFragment. When running on the server, it is a Fakement - an internal Rotors class that behaves like a DocumentFragment.

One important piece of the rendering process that the above call does not make clear is that Rotors needs some way of looking up template content from the template IDs you give it. By default, in a browser, Rotors will look for an element with the given ID, and then use that element's innerHTML. On the server there is currently no default behaviour, and so you have to use the three argument version of the template method:

var rotors = Rotors.newInstance();
var fragment = rotors.template("someId", { data }, function(tid) {
  return somehow_you_looked_up_the_content_of_tid;
});

Template Resolver

By default, Rotors (when running in a browser), will attempt to resolve templates by looking for a script element with the same ID as the ID of the request template:

function(id) {
  return document.getElementById(id);
}

When you make a call like this:

var rotors = Rotors.newInstance();
var fragment = rotors.template("someId", { data });

Rotors therefore looks for a script element with ID someId. This can, however, be overridden, by providing your own template resolver:

var rotors = Rotors.newInstance();
var fragment = rotors.template("someId", { data }, function(templateId) {
    // lookup the template somehow, and return a string.
});

Instance wide template resolver

You can also override the template resolved in the parameters you pass to Rotors.newInstance:

var rotors = Rotors.newInstance({
  templateResolver:function(id) {
     // get the template, and return it...
  }
});

Providing templates directly

A further option for template resolution is to provide a map of templates to the Rotors.newInstance method

var rotors = Rotors.newInstance({
  templates:{
    "foo":"<h1>FOO</h1>",
    "bar":"<h1>BAR</h1>"
  }
});

Default Template

You can set a default template to use if all other template resolution fails:

rotors.setDefaultTemplate("<div id=\"${id}\"></div>");

This can subsequently be cleared:

rotors.clearDefaultTemplate();

But note that any template that was rendered while the default template was available will still be in the cache, eg:

rotors.setDefaultTemplate("<div id=\"${id}\"></div>");
var e = rotors.template("someTemplateId", data);
rotors.clearDefaultTemplate();
var e2 = rotors.template("someTemplateId", data);

Here, both e and e2 will be a DocumentFragment containing a div element. If you need to clear out anything that was compiled with the default template, you'll also need to call clearCache().

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"{{if selected}} selected{{/if}}></input>

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

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:

<span>${title}</span>
<ul>    
    <r-each in="someDataMember">
        <li>${id}</li>
    </r-each>
</ul>

If you render it with this call:

var fragment = rotors.template("theTemplateId", {
  title:"FOO",
  someDataMember:[
    { id:"one" },
    { id:"two" },
    { id:"three" }
  ]
});

You'll get a span that says FOO, and a list of three items: one, two and three.

You can update from the root node:

rotors.update(fragment.childNodes[0], {
    title:"FOO-NEW",
    someDataMember:[
        { id:"un" },
        { id:"deux" },
        { id:"trois" }
    ]
}

You now have a span that says FOO-NEW, and a list of three items: un, deux and trois.

You can also update one specific element, you just need to ensure that the data you give it is in the appropriate format. Let's take the second li element and update it:

var li2 = fragment.querySelectorAll("li")[1];
rotors.update(li2, { id:"dos" });

Now the second list item is speaking Castellano and everyone else is speaking French.

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.

Update Notifications

You can get a notification about updates with the onUpdate method:

rotors.onUpdate(someElement, function(el, data) {
  // here, el is the element that changed
  // data is the new data applied to the element.
});