The design of Tierra Templates was heavily influenced by Django’s template inheritance. Template inheritance (as defined in Django docs) “allows you to build a base skeleton template that contains all the common elements of your site and defines blocks that child templates can override.”.
Template inheritance can be thought of as the inversion of template or server side includes – instead of working from the base template and including blocks you start with defining the blocks and work your way up the inheritance tree to the base template.
Let’s look at a simple example, starting with a base template called _page.html
<html>
<head><title>Example.com {@ pageTitle ? `- {$}` @}</title></head>
<body>
[@ start content @]
this is the default content
[@ end content @]
</body>
</html>
and then a let’s create our homepage at index.html:
[@ extends "_page.html" @]
<@ pageTitle = "Homepage" @>
[@ start content @]
Welcome to example.com!
[@ end content @]
When the index.html template is loaded the generated template code sets the pageTitle variable to “Homepage” and sets the contents of the content block. The index.html template then loads the parent _page.html template which sets the HTML <title> contents using the pageTitle in the conditerator and replaces the default content in the content block with the content from the index.html template.
The first example is pretty much exactly what you would do in Django or other templating systems like Dwoo that support inheritance. But like the late night infomercials say, that’s not all you can do with Tierra Templates. We’ve extended our inheritance system to support:
- prepending and appended content to blocks in parent templates
- echoing block content created by the template calling code or by the template’s children
- conditionally performing block operations with support for cascading else blocks
- block decorators which are functions called at code generation time allowing you to wrap blocks in code
- a page level psuedo block that lets you apply decorators to the content of entire page
Let’s see how each one of these extensions work:
Prepending and Appending Blocks
You can prepend and append content by using the “prepend” or “append” block keyword instead of “start”. Here is a simple example where the child template adds html inside the parent template’s head tag. First the parent template using _page.html again.
<html>
<head>
[@ prepend pageHead @]
<title>Test</title>
[@ end pageHead @]
</head>
</html>
and then the child template:
[@ extends "_page.html" @]
[@ prepend pageHead @]
<style>
a {text-decoration: none; }
</style>
[@ end pageHead @]
when the child template is output the <head> element will look like:
<head>
<title>Test</title>
<style>
a {text-decoration: none; }
</style>
</head>
Note that the parent block also used “prepend”. If it used “start” then the block contents set in parent would be ignored. Using “start” is equivalent to saying “if this block hasn’t been defined then set its contents to this…”. The parent could also use append but then the order of the output of the head would be switched and the <title> would come after the style.
Echoing Blocks
Echoing existing block content is a nice shorthand way to output block content created by the template’s calling code or its children. Using echo is functionally equivalent to starting a block and then immediately closing it. In code this looks like:
[@ echo content @]
instead of
[@ start content @][@ end content @]
Conditional Blocks
Conditional blocks use the same if syntax that conditerators use and support cascading else blocks. Let’s look at a simple example where a different message is shown depending on some application defined function that returns the user access level:
<@ userLevel = getUserLevel() @>
[@ start if userLevel == "admin" @]
you are an admin
[@ else if (userLevel == "editor") || (userLevel == "copyeditor") @]
you are an editor or copy editor
[@ else @]
you are a normal user
[@ end @]
Note that in conditional blocks you aren’t required to use a block name and the conditionals define the code path so if you have a function call in an if or else clause that is false then that function is not called.
Block Decorators
Block decorators are functions called by the code generator while the template is being compiled and give you a powerful way to plugin to the Tierra Template code generation process. They are not called when the cached template code is executed. Block decorators are called twice, once at the start of the block and once at the end. You can name multiple block decorators for a single block and you can remove a block decorator from a parent block. Let’s see how block decorators work with the built in memcache block decorator:
[@ start sidebar do memcache({for: "30 minutes", vary: "session['user_id']"}) @]
{@ runHugeQueryToGetSidebarLinks() ? "<ul>" `<li><a href="{link}">{text}</a></li>` "</ul>" @}
[@ end sidebar @]
When this template’s code is generated the memcache function is called with two arguements, the first is an automatically generated array with the calling context and the second is the array (json objects are deserialized to arrays) containing the template parameters. The built in memcache code uses the context and template parameters to wrap code around the block to lookup the block content in the memcached server using a block hash provided in the generated context array.
You can write your own block decorator functions very easily. As an example let’s write a decorator that wraps a block in start and end comments that you can use when you are viewing source in the browser. Let’s call it addHtmlComment. Here is what the function would look like:
function addHtmlCommentDecorator($context, $comment) {
if (!$context["guard"])
return "";
return "<!-- " . ($context["isStart"] ? "start" : "end") . " {$comment} -->";
}
you could then setup the search path to find the function (covered in a future blog post) and then use the decorator in your templates like this:
[@ start copyright do addHtmlComment("copyright") @] © {@ date("Y") @} [@ end copyright @]
this would output (in 2009 as I write this):
<!-- start copyright --> © 2009 <!-- end copyright -->
The check for the context guard is made because you can remove decorators from parent blocks in child templates. Let’s say for a page you don’t want to add the copyright html comment In the child template you would do this:
[@ start copyright do remove addHtmlComment @]
© {@ date("Y") @}
[@ end sidebar @]
this would cause the addHtmlComment decorator to not be called in the parent. Note that block decorators are called in parent templates even for blocks already defined in the children. This makes sense if you think about wanting to cache the main content block in the parent – you wouldn’t want to have to add the decorators to each child template that overrode the main content block. Also note that parameters to block decorators need to be literal values since the decorator is called at compile time. It can’t use template variables or request variables BUT the code that the decorator outputs can use them as that is run each time a template is loaded.
Page Blocks
Finally page pseudo blocks let you run block decorators with the content of the entire page. You can use the built in memcache decorator to cache the entire page varying by the page’s query string and logged in user and wrap it in an html comment by doing the following:
[@ page do memcache({for: "until midnight", vary: "get, session['username']"}), addHtmlComment("page") @]
note that the order is important – in this order the html comment is cached, if you reverse the order the html comment is added to the cached page and then output.
Conclusion
This introduction to the Tierra Template inheritence system was rather long but I hope it gives you an idea of the cool things it will enable you to do with your applications. Once you start using template inheritance you’ll never want to go back to using includes.