Obviel Template¶
Introduction¶
Obviel Template is a template language for Obviel. Obviel Template is deliberately a very minimal template language. The idea is that more complicated logic should be kept in JavaScript code, not templates, because it is much clearer to express it in JavaScript. You can do this by preprocessing the object that goes into the template, or by using data-func in the template.
Obviel Template is different from many template languages you might have encountered in that it is element-based and HTML aware, not text-based. Templates must be valid HTML fragments. If you try to construct a template with elements in places where they are not valid HTML, you will find that the browser will not produce that HTML. The advantage of being element-based is that the template language can directly operate on the DOM. You can for instance directly bind event handlers in the template language.
Obviel Template integrates with Obviel in an important way: sub-views can be rendered directly from the template. This allows these views to work in the standard Obviel way. They can for instance respond to events on the objects that they are representing, and rerender themselves. This is another benefit of Obviel Template being element-based.
Another powerful feature of Obviel Template is that it supports i18n markers directly in the template for the purpose of internationalization (i18n) of template texts. This is based on standard gettext, and supports pluralization. This way automatic extraction tools can be used to maintain translations of strings in the template. This feature exists in some server-side template languages (such as Zope Page Templates), but this is not commonly supported by client-side template languages.
Variables¶
Obviel-Template supports variable interpolation using {} markers in any element or attribute text. It looks like this:
<p>{content}</p>
If you combine that template with a JavaScript object with the following structure:
{
content: "Hello world!"
}
it will result in a rendered template like this:
<p>Hello world!</p>
What if the content property is not available in the JavaScript object, and is thus undefined? Obviel Template considers this an error and the template will fail with a RenderError exception.
Dotted names in variables¶
Objects can be nested:
{
a: {
b: "The value"
}
}
You can indicate such nested values in a variable using a dotted name:
<p>{a.b}</p>
Rendering the template with the object will result in this:
<p>The value</p>
If you refer to a dotted name that does not exist in the data, this is an error and the template won’t render; you get a RenderError exception.
Dotted names work whenever a variable can be used, so also works in other directives such as data-with, data-if and data-each, which we will discuss later.
Interpolating the current object: @.¶
In Obviel Template, special variables start with @, and @. means “the current object”. This can be useful for debugging templates: you can see what the current object is somewhere. The current object will be serialized to JSON into the rendered template.
Consider this data:
{
title: "Foo"
}
and this template:
<div>
{@.}
</div>
Rendering the template with the data will result in this:
<div>
{
title: "Foo"
}
</div>
data-with for scope control¶
You can change the object in which variables are found with the data-with directive. Given the same nested structure as before:
{
a: {
b: "The value"
}
}
you can express the template like:
<div>
<p data-with="a">{b}</p>
</div>
Rendering the template with the object will result in this:
<div>
<p>The value</p>
</div>
data-with="a" indicates that within the p element, lookups will be performed in the b object primarily (that is the scope of that element).
data-with must point to an object; it cannot point to strings or arrays or other values, and cannot point to a missing value either: these are RenderError errors.
data-if for conditionals¶
Sometimes you want to include an element only if a given value is true. For the purposes of data-if, a value is false if:
- it is false
- it is null
- it is undefined
- it is 0
- it is an empty string ""
- it is an empty array
- the value is missing entirely
Otherwise, the value is considered true.
With !, the behavior of data-if can be reversed: values that evaluate to false will be included.
Consider this template:
<p data-if="person">Hello {person.name}!</p>
<p data-if="!person">{person.name} is not present</p>
Here’s a data structure with person present:
{
person: {
name: "Evan"
}
}
rendering the template will result in this:
<p>Hello Evan!</p>
but if you have a data structure without person present, such as this:
{
}
the template will render like this:
<p>Evan is not present</p>
data-each for repetition¶
If you point to an array with data-each, it will render each entry in the array into the DOM. So with a data structure like this:
{
persons: [
{
name: "Bob"
},
{
name: "Steven"
}
]
}
and a template like this:
<ul>
<li data-each="persons">{name}</li>
</ul>
the result will be this:
<ul>
<li>Bob</li>
<li>Steven</li>
</ul>
Note that data-each changes the scope to each entry in the array. So, data-each changes the object in which the variables are looked up to each entry in the array, one by one.
If the array is empty, nothing will be rendered at all. So, this data:
{
persons: [
]
}
will result in this:
<ul>
</ul>
You can use the special variable @. to render simple arrays where each item is not an object in itself. For instance:
{
persons: [
"Bob", "Steven", "Jay"
]
}
and this template:
<ul>
<li data-each="persons">{@.}</li>
</ul>
will render like this:
<ul>
<li>Bob</li>
<li>Steven</li>
<li>Jay</li>
</ul>
@each variable¶
data-each supports the special variable @each. This variable contains some useful properties during the construction of loops:
- index - the index of the current iteration, starting at 0
- number - the number of the current iteration, starting at 1
- length` - the length of the list
- even - true if the index is an even number
- odd – true if the index is an odd number
For example:
<ul>
<li data-each="persons">{@each.index}</li>
</ul>
will render (with three items in the list) as follows:
<ul>
<li>0</li>
<li>1</li>
<li>2/li>
</ul>
To support nested use of data-each, @each also supports a variable named after the variable that is currently being looped through, in this example persons:
<ul>
<li data-each="persons">{@each.persons.index}</li>
</ul>
which will do the same as the previous template.
Let’s look at how this is used in a nested data-each. Here’s the data:
{outer: [
{inner: ['a', 'b', 'c']},
{inner: ['d', 'e']}
]}
And here’s a template:
<ul>
<li data-each="outer">
<ul>
<li data-each="inner">
value: {@.} outer: {@each.outer.number} inner: {@each.inner.number}
</li>
</ul>
</li>
</ul>
If you used this template with the data, you would get this output:
<ul>
<li>
<ul>
<li>
value: a outer: 1 inner: 1
</li>
<li>
value: b outer: 1 inner: 2
</li>
<li>
value: c outer: 1 inner: 3
</li>
</ul>
</li>
<li>
<ul>
<li>
value: d outer: 2 inner: 1
</li>
<li>
value: e outer: 2 inner: 2
</li>
</ul>
</li>
</ul>
What if the variable data-each goes through is a dotted name - how is it used with data-each? You can addres it by replacing the periods by underscores. For instance, given this data:
{
something: {
persons: ["Bob", "Steven", "Jay"]
}
}
and this template:
<ul>
<li data-each="something.persons">{@each.something_persons.number}</li>
</ul>
you will get this as a result:
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
So to address something.persons in a loop, you use something_persons, with the . replaced by _.
To support various user interface you want to add a class into the DOM depending on a some @each value, such as even or odd. While you can do this with data-attr (which we’ll see later), we recommend you use data-func for this (which we’ll also see later), as your code will generally be clearer.
Nested scoping¶
Values in outer scopes are available for variables in inner scopes. Consider this structure of objects:
{
a: {
b: "The value"
},
c: "Outer value"
d: {
e: "Value in outer object"
}
}
In the scope a, we can still access c and d (and even a again):
<p>b in outer scope: {a.b}</p>
<p>c in outer scope: {c}</p>
<p>e in outer scope: {e}</p>
<p data-with="a">b in scope a: {b}</p>
<p data-with="a">c in scope a: {c}</p>
<p data-with="a">e in scope a: {d.e}</p>
<p data-with="a">b in a in scope a: {a.a.b}</p>
But if you redefine a name in an inner scope, you cannot access the value in the outer scope:
{
a: {
b: 'redefined'
}
b: 'original'
}
<p data-with="a">b in scope a: {b}</p>
will therefore get you:
<p>b in scope a: redefined</p>
data-view¶
Consider the following data:
{
greeting: "Hello",
who: {
iface: 'person',
name: "Pat Pending",
url: "http://www.example.com/pat_pending"
}
}
and this template:
<p>{greeting} <span><a data-with="who" href="{url}">{name}</a></span></p>
it will render like this:
<p>Hello <span><a href="http://www.example.com/pat_pending">Pat Pending</a></span></p>
This works. But we could also arrange our code so that we have a view for the person iface and use that instead. The advantage of that is that we could reuse this view in other places, and in addition we can use Obviel view features, such as binding its methods to DOM events (which we’ll see later). Let’s consider a view:
obviel.view({
iface: 'person',
obvt: '<a href="{url}">{name}</a>'
});
This is a view for the iface person. We use the Obviel Template (indicated by obvt) to render the view.
Now we can change our template to render who as a view:
<p>{greeting} <span data-view="who"></span></p>
The result is still the same:
<p>Hello <span><a href="http://www.example.com/pat_pending">Pat Pending</a></span></p>
Another advantage of using data-view in this case is that you have made the outer template more generic. If the object indicated by the name who has another iface, the template will still work unchanged. For example, here is a data structure where who is a robot:
{
greeting: "Greetings",
who: {
iface: 'robot',
designation: "OVL-R 4711",
}
}
and this is a view for the iface robot:
obviel.view({
iface: 'robot',
obvt: '{designation}'
});
and here is our outer template again, unchanged:
<p>{greeting} <span data-view="who"></span></p>
when we render the data now, we get this:
<p>Greetings <span>OVL-R 4711</span></p>
in other words, if you add data-view to an element el to render object who, Obviel Template will do the equivalent of this:
$(el).render(who);
Sometimes you have multiple views for an iface, distinguished by name. You can indicate the name used by data-view with a | character. Here’s another view for person with the name extended:
obviel.view({
iface: 'person',
name: 'extended',
obvt: 'The person has the name <a href="{url}">{name}</a>'
});
and here is how you refer to this extended view:
<p>{greeting} <span data-view="who|extended"></span></p>
By default, data-view looks up a view named default – the default view name for Obviel. If you want to change the way templates look up views to use another view globally you can do so in JavaScript:
obviel.template.setDefaultViewName('summary');
From now on if you don’t specify the view name explicitly, Obviel will look up views by the name of summary by default. If you decide to do this, you would want to do this once per web page before template rendering takes place, for instance when the page has just been loaded. Note that this does not alter the default view name of Obviel itself when you use el.render(obj); this will remain default.
data-view can also point to a variable that is a string, not an object. In this case the variable is treated as a URL, and this is loaded as a JSON object. The view is then rendered for that object.
data-view must point to an object or a string. It cannot point to an array or other value.
Formatters¶
If you need to insert HTML snippets, use data-view. If you must manipulate text in some consistent way, you can use formatters. Formatters can be registered globally with Obviel Template like this:
obviel.template.registerFormatter('upper', function(value) {
return value.toUpperCase();
});
This registers a formatter upper which when used will turn any string variable to upper case. Here’s how we would use it in a template:
<p>{foo|upper}</p>
if we render this with the data structure:
{foo: 'hello'}
we’d get the following output:
<p>HELLO</p>
We’ve seen a way to register formatters globally. Formatters can also be defined as methods on the view that uses the template:
obviel.view({
iface: 'foo',
obvt: '<p>{foo|upper}</p>',
upper: function(value) {
return value.toUpperCase();
}
});
Again, formatters cannot be used to include HTML, only text; for including HTML you can use views. To do some special DOM manipulation, you can also consider using data-func.
If you are registering global formatters, you should register formatters only once per web page, before template rendering starts.
data-handler: binding DOM event handlers¶
Often in a view you want to handle particular DOM events for elements in the view. You can bind DOM events to methods of the view using the data-handler directive.
For instance, if you want to bind a handler for the click event on a div, you can do it like this:
<div data-handler="click|someFunction">Click here!</div>
When the user now clicks on the div element, the function indicated by someFunction will be executed. someFunction is a method on the view that uses this template. Let’s look the example in a view:
obviel.view({
iface: 'foo',
obvt: '<div data-handler="click|someFunction">Click here!</div>',
someFunction: function(ev) {
alert("You clicked!");
}
});
To bind more than one event handler on an element at the same time, separate them by a space character:
<div data-handler="click|onClick dblclick|onDblclick">Click here!</div>
It is a RenderError if the handler function is not available on the view during render time.
data-unwrap: element that disappears¶
Obviel Template has element-based directives (data-if, data-each, etc). But sometimes you just want to conditionally or repeatedly include a piece of text, not an element. You can do this by using a special data-unwrap element. During template rendering all elements marked by data-unwrap disappear.
So:
Hello? <span data-unwrap="" data-if="again">HELLO?</span>
with this data:
{ again: true }
will render like this:
Hello? HELLO?
and with again set to false:
{again: false}
will render like this:
Hello?
data-el: dynamically generating elements¶
Normally you would generate an element by simply including it in the template, for instance a p element like this:
<p>Hello world</p>
In some cases you want to be able to dynamically decide the name of the element based on the data. data-el lets you do this. This is the equivalent of the above template using data-el:
<div data-el="p">Hello world</div>
When this is rendered, you’ll see this just like with the first example:
<p>Hello world</p>
What this lets you do is dynamically generate element names:
<div data-el="{myElementName}"></div>
You could also use data-repeat to generate a whole series of elements:
<div data-el="{@.}" data-repeat="tags"></div>
data-attr: dynamically generating attributes¶
Normally you’d generate a dynamic attribute like this:
<a href="{url}">{title}</a>
but sometimes this isn’t good enough: in particular, we sometimes want to be able to generate attributes that are only there if some condition is true. Obviel Template has a special data-attr directive that lets you do this. The equivalent of the above example using data-attr looks like this:
<a><span data-attr="href" data-value="{url}"/>{title}</a>
What will happen when this template is rendered is the same as in the first example:
<a href="/some/url">Some Title</a>
data-attr will always apply to the element it is directly contained in. Note that the element that has the data-attr on it itself is never inserted in the result.
This construct is helpful because you can use data-if for conditional attributes now:
<a><span data-if="flag" data-attr="href" data-value="{url}" />{title}</a>
Now, the href attribute will appear only if flag is true.
In combination with data-each this can be used to dynamically generate multiple attributes from data:
<a><span data-each="attributes" data-attr="{name}" data-value="{value}"/>{title}</a>
That template, combined with this data:
{
title: "Hello world",
attributes: [ {name: 'href', value: 'foo'},
{name: 'class', value: 'bar'} ]
}
would result in the following HTML:
<a href="foo" class="bar">Hello world</a>
If you generate the same attribute multiple times the content is added to the existing attribute, with a space character for a separator. This is for instance useful when you want to add multiple classes dynamically to an element and have some show up only if a condition is true. This:
<div>
<span data-attr="class" data-value="{first}" />
<span data-attr="class" data-if="flag" data-value="{second}" />
</div>
combined with this data:
{
first: 'first',
second: 'second',
flag: true
}
will result in this output:
<div class="first second"></div>
but with this data:
{
first: 'first',
second: 'second',
flag: false
}
you’d get this output:
<div class="first"></div>
But as we say in the sidebar, consider using data-func instead in these cases.
data-func¶
In many cases it is more convenient to use JavaScript and jQuery to manipulate the DOM directly than it would be to use Obviel Template constructions such as data-attr. Obviel Template offers a special directive called data-func that lets you plug custom functions into the template rendering process directly.
To see what data-func does, let’s consider a simple case first. Let’s imagine these two individuals:
{
iface: 'person',
name: "Jane"
wild: false
}
{
iface: 'person',
name: "Tarzan",
wild: true
}
We now want to render a person but want to add a wild class to an element in the view only if that person is wild. We could do this in a relatively cumbersome way, with data-attr and data-el:
obviel.view({
iface: 'person',
obvt: '<div><span data-if="wild" data-attr="class" data-value="wild" />{name}</div>'
});
Let’s rewrite this to use data-func and a method on the view instead:
obviel.view({
iface: 'person',
obvt: '<div data-func="attributes">{name}</div>',
attributes: function(el) {
if (this.obj.wild) {
el.addClass('wild');
}
}
});
Especially for more complicated logic, this is often a lot more easy to understand.
The method called by data-func gets an optional second argument, variable. This is a function you can use to get to variables in the same scope as the template does where data-func was defined. Let’s rewrite the view to use variable:
obviel.view({
iface: 'person',
obvt: '<div data-func="attributes">{name}</div>',
attributes: function(el, variable) {
if (variable('wild')) {
el.addClass('wild');
}
}
});
It’s also possible to register a data-func function globally. It will look for these if it cannot find it on the view itself. Here is a silly example that adds a magic attribute:
obviel.template.registerFunc('magic', function(el) {
el.attr('magic', 'Magic!');
});
Once this registration has been made, we can use it within any template, for instance this one:
<p data-func="magic">Hello world!</p>
And once the template is rendered, we’ll see this:
<p magic="Magic!">Hello world!</p>
data-func is especially useful in combination with data-each. Let’s consider the case where we want to add an even and odd class to each iteration. We could write a template like this:
<ul>
<li data-each="list">
<p data-if="@each.even" class="even">{@.}</p>
<p data-if="@each.odd" class="odd">{@.}</p>
</li>
</ul>
But this template has various problems:
- we have to write the p bit twice
- what if we wanted to place the class on li instead of on p?
We could also use dynamically generated attributes to solve this case:
<ul>
<li data-each="list">
<p>
<span data-if="@each.even" data-attr="class" data-value="even"/>
<span data-if="@each.odd" data-attr="class" data-value="odd" />
{@.}
</p>
</li>
</ul>
This would also allow us to generate the class attribute on the li:
<ul>
<li data-each="list">
<span data-if="@each.even" data-attr="class" data-value="even"/>
<span data-if="@each.odd" data-attr="class" data-value="odd" />
<p>{@.}</p>
</li>
</ul>
This a reasonable solution. But it’s still pretty verbose, and it’s just so easy to express a rule like this in JavaScript with jQuery:
var evenodd = function(el, variable) {
if (variable('@each.even')) {
el.addClass('even');
} else {
el.addClass('odd');
}
};
You could have this evenodd function on the view that needs it. But in case of a generally useful functionality like this you can plug it in globally for the application:
obviel.template.registerFunc('evenodd', evenodd);
And then we can use it on p:
<ul>
<li data-each="list">
<p data-func="evenodd">{@.}</p>
</li>
</ul>
Or to use it to add the class to li directly:
<ul>
<li data-each="list" data-func="evenodd">
<p>{@.}</p>
</li>
</ul>
We can now also support more complex situations, where we don’t want to add a class attribute on even or odd, but every third item:
var everythird = function(el, variable) {
if (variable('@each.index') % 3 === 0) {
el.addClass('third');
}
};
obviel.template.registerFunc('everythird', everythird);
and then use it:
<ul>
<li data-each="list" data-func="everythird">
<p>{@.}</p>
</li>
</ul>
everythird cannot be expressed using Obviel Template alone, but is easy to express using a custom fuction.
So using data-func you can supplement Obviel Template with functionality particular to your views or application. This is useful for particular functionality your view needs, or for general functionality your whole application needs.
There are some restrictions on data-func. You should only use data-func to change the element, for instance by manipulating its attributes, not to add or remove child elements (or text-nodes). Most DOM manipulations by adding, moving or removing elements or text modes are unsafe: it can break Obviel Template in unexpected ways, as it relies on the order of child nodes to stay the same. But if you want to insert a whole bunch of elements you should consider using data-view instead.
special attributes (id, src)¶
Since Obviel templates use HTML and the HTML compiler internally, Obviel templates should be valid HTML fragments.
In valid HTML, an id attribute should be unique. If the id attribute is hardcoded there is no problem; you can just use them:
<div id="foo"></div>
But variables in ids are not allowed, because it would be too easy to have a HTML fragment with the same id (such as {myId}) in multiple id attributes. So this is illegal:
<div id="{myId}"></div>
If you try this, you’ll get an compilation error.
To generate an id dynamically, you can instead use the data-id directive:
<div data-id="{myId}"></div>
This works just like any other attribute, so you can put things before and after the variable:
<div data-id="prefix{something}postfix"></div>
This will generate HTML with proper id attributes.
A similar story applies to the src attribute. Let’s consider the img element. When the template is being parsed, the image referred to by src is loaded by the browser immediately. So this in your template would be okay:
<img src="myimage.png" />
But this is illegal:
<img src="{myImage}" />
because we wouldn’t want the browser to look for a URL {myImage} literally. Obviel Template prevents this by giving you a compilation error when you try this.
Instead, you can use data-src to set the src attribute:
<img data-src="{myImage}" />
data-trans for i18n¶
Obviel Template offers the ability to write a template that can be shown in other languages as well: you can internationalize a template. Here we will discuss the basics of marking up a template so it can be translated, but much more detail is in a separate document about template i18n.
If we have a template like this:
<p data-trans="">Hello world!</p>
we can also show this template in another language, such as Dutch, if the appropriate translation is available:
<p>Hallo wereld!</p>
data-trans is used to mark up those parts of the template that should be translatable, such as attributes and texts.
A translatable text can contain variables as well:
<p data-trans="">Hello {who}!</p>
The data-trans attribute without content is used to mark the textual content of a tag for translation. If you want to mark an attribute as translatable you can do it like this:
<div data-trans="title" title="Hello" />
You can also mark multiple attributes:
<div data-trans="title other" title="Hello" other="Something" />
You can indicate attributes and textual element content with the special name .:
<div data-trans=”. title” title=”Hello”>Contents</div>
Read Obviel Template i18n for much more detail.
Order¶
The order of execution of operations on an element is as follows :
- data-if
- data-each
- data-with
- data-view or data-trans
- data-attr
- data-func
- data-el, data-unwrap
For example, you could combine directives like this:
<p data-if="a" data-each="b" data-with="c" data-view="d"></p>
This means that if the value a is present and true, the p element will be included in the rendered template (at all). After this, for each of the entries in b (so b scope, iteratively), we render the object d in the c scope with a view.
This order holds no matter in what order you define the attributes.
data-trans is at the same level as data-view. It’s not allowed on an element with data-view.
data-attr and data-func are executed last for an element, and finally elements are transformed using data-el or data-unwrap.
Literals¶
Injecting the characters { and } can be done using a special markup:
{@open}
{@close}
You can also inject the pluralization marker || literally like this:
{@doublepipe}
Unsupported XML constructs¶
The HTML CDATA section support in browsers is so inconsistent we didn’t think it was worth spending time supporting this in templates, given its limited utility. Obviel Template therefore has undefined behavior in the presence of CDATA sections in your template: we recommend you don’t use them. If you want to insert extended sequences of text in an element you can already do so with a simple variable.
Processing instructions are also not supported.