Contents |
Introduction
What is TALE?
TALE (Template Attribute Language for ECMAscript) is Axiom's native templating system. It works as a series of transformations on an XML document and is designed to be naturally integrated with the Javascript an application is written in.
When you create a .tal file inside of a prototype's directory, Axiom wraps it up in a Javascript function and tacks that function onto the prototype. For example, creating the TALE file named sidebar.tal in the HomePage directory will create a function named sidebar on the HomePage prototype. When called, this function returns the XML object that results from the TALE transformation of the XML in the file.
You got Javascript in my XML!
At a basic level, TALE expressions are Javascript expressions set on XML attributes. The expressions are evaluated in the current Javascript scope, and the result is used to transform the document they are contained in. Before we get into a detailed discussion of scope, let's take a look at a trivial example. Let's say we have a file in under HomePage/guide.tal:
<div xmlns:tal="http://axiom.com/">
The answer is <em tal:content="21*2"></em>. What was the question?
</div>
Inside a Javascript function defined on HomePage, we call this.guide(). The result is this XML object:
<div>The answer is <em>42</em>. What was the question?</div>
Let's take a look at the original TALE document. The first thing to note is the xml namespace on the outer div node. The tal namespace needs to be set to "http://axiom.com/" so Axiom will know to treat the document as TALE to be transformed. Next, take a look at the em node. It has the attribute tal:content, with a value of 21*2. That arithmetic expression, when evaluated as Javascript, returns the value 42. The tal expression takes this result and makes it the content of the em node.
Scope
As mentioned above, every TALE expression is executed in a Javascript scope. There are three objects always available:
- this: Reference to the object on which the TALE is being executed. For example, if we invoked a TALE function on an instance of a HomePage prototype- my_home_page.some_tale()- then 'this' inside that TALE file would refer to that HomePage instance.
- node: The current node in the XML being operated upon. In which gives you access to the attributes and children via E4X - eg. node.@href is the current href attribute
- data: The current scope itself. This is a Javascript object that acts as an associative array, mapping variable name to value.
Beyond that, the initial scope is determined by the arguments to the TALE function call. If the function is called with no arguments, the only objects in scope are the three above. If a JSON object is passed as the first argument, then it's properties are added to the scope available to the file. Let's take a look at how this scope affects the evaluation of TALE.
Given a file pirates.tal:
<div xmlns:tal="http://axiom.com/">
Yo ho ho and a bottle of <em tal:content="booze"></em>!
</div>
If we call this file with
this.pirates({booze: "fine wine"});
Then the result is:
<div>
Yo ho ho and a bottle of <em>fine wine</em>!
</div>
See what we did there? The first argument to the TALE invocation actually becomes the scope for the TALE file. Any property defined in that object is now a variable available to all TALE expressions therein.
Calling TALE from within TALE
A core principal of good programming practices is DRY- Don't Repeat Yourself. TALE is designed to make it easy to reuse common block of templates across your application.
Let's look at an example. If an application has a dynamically generated navigation that appears on every page of every object, it makes sense to factor it out to a single file. We could create the file navigation.tal(see Reference below for a detailed discussion of the tal directives used here):
<ul xmlns:tal="http://axiom.com" class="nav">
<li tal:repeat="item: this.getNavObjects()"><a tal:attr="href: item.getURI()" tal:content="item.title"/></li>
</ul>
We can then include this wherever we want by simply calling that function:
<!-- fragment --> <div class="nav-wrapper" tal:content="this.navigation()"/>
Gotchas
Non-valid XML
All TALE templates must be valid XML. A common mistake is to include elements in client-side Javascript (or elsewhere in the document) that break XML well-formed-ness. For example:
<script type="text/javascript"> if(x > y && y < z){ doTheFunkyChicken(); } </script>
The solution here is to pull the inline script out to an externally loaded file, or to wrap it in a CDATA block:
<script type="text/javascript">
//<![CDATA[
if(x > y && y < z){
doTheFunkyChicken();
}
// ]]>
</script>
If you need to include entities elsewhere in your browser-bound templates, just use the numerical entities: © for ©, for example.
Undefined Variables
Just like in Javascript, referencing an undefined variable in TAL will cause a reference error. E.g., calling
this.battle({robot: "Optimus Prime", monkey: "King Kong"});
Where battle.tal is:
<div xmlns:tal="http://axiom.com/"> <em tal:content="robot"/> vs. <em tal:content="monkey"/> vs. <em tal:content="ninja"/> </div>
Unless the variable ninja has been defined earlier in the TALE file, then this will produce a reference error. The solution, most times, is simply to be sure your variables are declared before referencing them, just as you would in procedural code. Sometimes, however, you want to reference variables that may not be defined- for example, error messages that are only defined when an error has occured. We can take advantage of the fact that our scope is an object we can reference:
<form action="some_action">
<span content-if="data.text_error"/>
<input name="text/>
<input type="submit"/>
</form>
This will display the value of the text_error variable if it is defined (and is not false) in the current scope, and will omit the span node if it is not.
Reference
Order Of Operations
The TALE evaluator does an in-order traversal on the XML tree, evaluating the TALE on each node. TALE attributes on each individual node are evaluated in the following order: repeat, var, eval, if, repeat-content, content-if, attr, content, replace, text, omit.
Commands
tal:content
tal:content="expression"
Replaces the children of the node with the value of expression.
Example:
<h1 tal:content="this.header"> Some poor hapless content <a href="secret/of/life/42.html">that you'll never see</a>.</h1>
Result:
<h1>Value of the expression "this.header".</h1>
tal:content-if
tal:content-if="expression"
Convience form of tal:content. If expression evaluates to false, then the node is deleted. Otherwise, it operates just like tal:content- the result of expression replaces the children of the node.
Example:
<body><h1 tal:content-if="this.optionalHeader"></h1></body>
If this.optionalHeader is false:
<body></body>
Otherwise:
<body><h1>Value of the expression "optionalHeader".</h1></body>
tal:replace
tal:replace="expression"
Replaces this node and its children with the value of "expression".
Example, where user.getUsername() returns 'leeroy_jenkins':
<b>Welcome <span tal:replace="user.getUsername()">Username</span>!</b>
Becomes:
<b>Welcome leeroy_jenkins!</b>
tal:attr
tal:attr="name: expression[, attr-expression...]"
Assigns the value of each expression to the node's attribute of that name. If the expression evaluates to false, then the attribute is removed from the node.
Example:
<a tal:attr="href: this.getURI('more_news')">More...</a>
Becomes:
<a href="/my/object/uri/more_news">More...</a>
NOTE: If the attribute you are setting is a JavaScript reserved word (class, for, etc.), then you will need to quote the attribute.
<a tal:attr="'for': propname">More...</a>
tal:text
tal:text="escape-character"
Replaces all strings matching escape-character{javascript-expression} in the text nodes that are immediate children of this element.
Example, where session.user.username evaluates to 'leeroy_jenkins':
<div tal:text="$">Logged in as: ${session.user.username}</div>
Becomes:
<div>Logged in as: leeroy_jenkins</div>
tal:if
tal:if="expression"
If expression evaluates to false, then this node and all descendents will be deleted.
Example:
<div><a href="/login" tal:if="!authenticated">Login</a></div>
If !authenticated evaluates to false:
<div></div>
Otherwise:
<div><a href="/login">Login</a></div>
tal:omit
tal:omit ="expression"
If the expression evaluates to true, removes the node but leaves its children in place.
Example:
<div><a href="first_page" tal:omit="current_index == 0">First Page</a></div>
If current_index == 0:
<div>First Page</div>
Otherwise:
<div><a href="first_page">First Page</a></div>
tal:repeat
tal:repeat="name: expression"
Walks over the properties of the result of expression with the iterator variable name, repeating the element and it's children once for each property. This works like the for each..in statement in Javascript; tal:repeat="foo: bar" is analogous to for each(foo in bar).
Example:
<table>
<tr tal:repeat="employee: this.getEmployees()">
<td tal:content="employee.first_name"></td>
<td tal:content="employee.last_name"></td>
</tr>
</table>
If this.getEmployees() returned three employees, this would become (something like):
<table>
<tr><td>Leeroy</td><td>Jenkins</td></tr>
<tr><td>Arthur</td><td>Dent</td></tr>
<tr><td>Ford</td><td>Prefect</td></tr>
</table>
With each repetition, the name variable takes on the value of the next item in the collection or object being iterated over. Since we are iterating over values (rather than the indicies or property names), one cannot get information about our current place in the iteration from the name variable directly. Instead, this is provided via the special repeat variable. This is in scope inside any tal:repeat or tal:repeat-children (see below), and is accessed by repeat.variable-name. The following properties and functions are available on this object:
- index - Number. Index of in the sequence, starting from zero.
- number() - Function. Returns index in the sequence, starting from one.
- even() - Function. Returns true if this is an even index in the sequence.
- odd() - Function. Returns true if this is an odd index in the sequence.
- start() - Function. Returns true if this is the first item in the sequence.
- end() - Function. Return true if this is the last item in the sequence. This is never true if we are iterating over the properties of an object.
Example for alternating shading on a list:
<ul>
<li tal:repeat="item: items">
<span tal:attr="'class': (repeat.item.even()) ? 'shade-on' : 'shade-off'" tal:text="$">
${item.name}
<hr tal:if="!repeat.item.end()"/>
</span>
</ul>
Becomes:
<ul>
<li><span class="shade-on"> Aardvark </span></li>
<li><span class="shade-off"> Bovine </span></li>
<li><span class="shade-on"> Cephelopod </span></li>
<li><span class="shade-off"> Dingo <hr/> </span></li>
</ul>
tal:repeat-children
tal:repeat-children="name: expression"
Identical to tal:repeat, except that only the child nodes of the element in question are repeated.
Example:
<p tal:repeat-children="employee: this.getEmployees()"> <b tal:content="employee.first_name"/> - <i tal:content="employee.last_name"/> <br/> </p>
Becomes: <p> <b>Leeroy</b> - <i>Jenkins</i> <br/> <b>Ford</b> - <i>Prefect</i> <br/> <b>Arthur</b> - <i>Dent</i> <br/> </p>
tal:eval
tal:eval="expression"
Evaluates the expression in the existing scope. As with eval in Javascript, this should be used sparingly, and never where another tal construct can do the same for you. The example below could be done more simply with tal:attr.
Example:
<a href="http://test.com" tal:eval="node.@href+=this.getURI()">test-o-rama</a>
Becomes:
<a href="http://test.com/uri/of/this/object">test-o-rama</a>
tal:var
tal:var="name: expression [, var-expression...]"
This works similar to the var keyword in Javascript. The value of the attribute is a comma seperated series of 'name: expression' statements that each create a variable name with the value of expression. This establishes a new scope- any variable defined here is visible only to the tal:var's element and it's children.
Example:
<tr tal:var="item: this.someExpensiveQueryOperation()"> <td tal:content="item.prop1"/> <td tal:content="item.prop2"/> <td tal:content="item.prop3"/> </tr> <!-- 'item' isn't visible past the close of the element it was defined on -->
