
There have been recurring requests about an in-depth explanation of how Ozone — our in-house OCaml web framework — handles HTML templates. So, here it is.
A template is usually understood by everyone to be « HTML with holes » that is filled using values from the application itself. It is, in a sense, a DSL that is restricted to describing how HTML should be built.
Here is an example of template Ozone could use, stored in file users.htm :
<h1>{t:users.title}</h1>
<ul class="userlist">
{{list:
<li id="{v:id}">
<img src="{v:img}"/>
<a href="{v:url}">{v:name}</a>
</li>
}}
</ul>
<script type="text/coffeescript" params="save_url">
list = @$.find 'ul'
save = =>
ids = [];
list.children('li').each ->
ids.push $(@).attr 'id'
@ajax save_url, ids
list.children('li').sortable
change: save
</script>
<style type="less">
.userlist {
list-style-type: none;
li img {
float: left;
margin-right: 5px;
width: 50px;
height: 50px;
}
}
</style>
The template for a sortable list of users contains three things:
- A piece of HTML, which is the actual « HTML with holes » to be filled later. The holes are marked in orange.
- A piece of CoffeeScript, which will be extracted from the template file, compiled to javascript and appended to a site-wide javascript file. It will be replaced, in the template, by a hole that will call the extracted javascript with additional parameters provided by the application (in orange).
- A piece of LESS CSS, which is compiled to CSS and appended to a site-wide CSS file.
These are not sections — they can appear in any order as long as the elements and attributes are respected so the pre-build tool can identify and extract the CoffeeScript and CSS bits.
Let’s examine each of them in order.
The HTML Template
This is the meat of the template. In order to improve application performance, loading the templates is a multi-step operation that involves intermediary storage formats.
The first step consists in reading in all the necessary templates, parsing them to determine that no variables are undefined, and storing them as a JSON blog in the underlying CouchDB database. This is a manually triggered operation that happens whenever we modify the templates (it’s part of our deployment procedure). This step may also involve a bit of cleanup, such as removing semantically irrelevant spaces from the HTML (this cannot be done earlier, because some templates are plaintext instead of HTML, and only the application knows which is which).
The second step happens whenever a new instance of our application begins — maybe it died and needed to restart, maybe Apache decided it needed another worker process to handle a surplus of request, or maybe we added a new server to our web farm. The startup process of our application server does not read anything from the disk — instead, it will read in all the template data from the database, along with all the other bits of configuration: internationalization strings, third party API keys, feature branch triggers, and so on. Then, it will compile every template down to optimized closure-based opcodes for a hole-filling virtual machine.
The third step happens whenever a bit of HTML needs to be rendered. The application provides the hole-filling virtual machine with a data object and a « writing stream » which is either the HTTP request stream or a JSON serializer stream, depending on whether the request is normal HTTP or AJAX. This is an extremely fast operation where no parsing or checks are performed.
On the application side, loading a template involves three things:
- Declaring the type of the data object expected by the template.
- Declaring the source file from the template (as a function of the language).
- Declaring the hole-to-value mapping to be used.
Here’s that loading code for the above template file:
module User = Loader.Html(struct type t = < id : Id.t ; url : string ; img : string option ; name : string > ;; let source _ = "users/list" let mapping _ = [ "id", Mk.esc (fun x -> Id.to_string (x # id)) ; "url", Mk.esc (fun x -> x # url) ; "img", Mk.esc (fun x -> BatOption.default img404 (x # img)) ; "name", Mk.esc (fun x -> x # name) ] end) module UserList = Loader.Html(struct type t = < users : User.t list > ;; let source _ = "users" let mapping lang = [ "list", Mk.list (fun x -> x # users) (User.template lang) ] end)
One view is defined and loaded for every independent piece of HTML in the template. Here, there is an User view which represents the list item for a single user, repeated zero, one or more times ; and there is the UserList view representing the wrapper in which those list items will be placed.
The {v:foobar} syntax defines a variable hole. The corresponding view MUST define a mapping for that variable, or an error will occur at deployment time.
The {{foobar: }} syntax is a variant: in addition to declaring a variable hole, it also defines such a sub-view, which can be loaded using template/foobar as the path.
The {t:foobar} syntax defines a translation hole. The template engine will automatically load the corresponding term from the internationalization dictionary used to render the template.
The Mk.esc and Mk.list are binding instructions which are used to compile the template to a virtual machine. The common binding instructions are:
Mk.esc fappliesfto the data object, which returns a string. That string is then HTML-escaped and output.Mk.str fis the same as above, but the string is not HTML-escaped.Mk.i18n fis the same as above, but the string is translated as an internationalization term.Mk.list f tappliesfto the data object, which returns a list of data objects compatible with templatet. That template is then used to render those data objects in order.Mk.list_or f t eis the same as above, but if the returned list is empty, it instead uses templateeto draw a « list is empty » message.Mk.sub f tappliesfto the data object, which returns a single object compatible with templatet. That template is then used to render the object.Mk.sub_or f t eis the same as above, butfreturns an optional type. If it is missing, then templateeis used to render an « object is missing » message.Mk.text fprovidesfwith the current writing stream and internationalization object, so that it may directly write HTML to the output. This is how most rendering helpers such as « render a currency amount » are used.Mk.box fis the same as above, but the writing stream supports the addition of arbitrary javascript code to be executed by the client as part of rendering the template. This is how javascript-dependent rendering helpers such as « render a datepicker » are used.
The data type is defined in the view itself, either explicitly (as I did above for the sake of clarity) or by using an existing type from your application — if the application already had an user module with the appropriate data type, I could have used that type instead.
By specifying views in this way, the data required to render a template is made available to the compiler for type-checking, and missing bindings are detected during deployment (usually to a local test server). This has made template-related errors exceedingly rare — once the HTML is done, it becomes extremely hard to use it wrong.
Although this feature is not currently in use, the virtual machine semantics also allow compiling it down to JavaScript. This would allow us to send the rendering code to the client as a one-time cost, and send a much smaller data package through AJAX whenever something new needs to be rendered.
The CoffeeScript Layer
We use CoffeeScript because it’s more elegant, shorter, and includes a compiling-to-javascript step that lets us detect syntax errors at deployment time. Yes, compile- and deployment-time are my favorite buzzwords, because I enjoy the feeling of safety that they bring.
As mentioned above, the actual CoffeeScript is removed from the template in a pre-processing step, and replaced with a hole that says « call JavaScript function #33 now » that happens to define a list of parameters matching the params attribute of the original script element.
So, starting with the script element from the example above:
<script type="text/coffeescript" params="save_url">
list = @$.find 'ul'
save = =>
ids = [];
list.children('li').each ->
ids.push $(@).attr 'id'
@ajax save_url, ids
list.children('li').sortable
change: save
</script>
If this is the 33rd script tag encountered by the preprocessor, then it would append the following to the complete CoffeeScript file:
@j33 = (save_url) -> list = @$.find 'ul' save = => ids = []; list.children('li').each -> ids.push $(@).attr 'id' @ajax save_url, ids list.children('li').sortable change: save
And it would be replaced in the template file with this:
{j:j33:save_url}
This syntax (which can be used manually, although it should be avoided) is a javascript hole, it runs the specified function and provides by-name values for the arguments. The parser would notice that we are declaring an HTML view instead of a JS/HTML view and complain about it, so we would have to go back and re-define it:
module UserList = Loader.JsHtml(struct type t = < users : User.t list ; save_url : string > ;; let source _ = "users" let mapping lang = [ "list", Mk.list (fun x -> x # users) (User.template lang) ] let script _ = [ "save_url", (fun x -> Json_type.String (x # save_url)) ] end)
I have used Loader.JsHtml instead of Loader.Html, and defined a secondary mapping that is specific to JavaScript parameters, and which uses the data object to return JSON values.
How is the JavaScript called? Well, it really depends on how your JavaScript library handles it. On non-AJAX HTTP, Ozone will try to inject all JavaScript calls in a script element at the end of the HTML body. In AJAX mode, Ozone allows you render a template to a JSON object representing both the HTML and the JavaScript together, and it is the responsibility of the code that made the AJAX request to receive that object, place the HTML wherever applicable, and then “run the JavaScript”.
By convention, the JavaScript is called using a client context as itsthis value. The client context is an object which may contain whatever the caller finds interesting to place there, along with a variable named $ which should be a jQuery selection containing the root element of the previously rendered HTML. Hence, @$.find 'ul' would select the list in the rendered HTML, instead of all the lists on the page.
The LESS CSS Layer
This is the least interesting of all three layers. The LESS CSS code is extracted, appended to a single file, and compiled to CSS (which, again, is an useful deployment-time syntax check). The point of this feature is simply to let the designer place element-specific CSS next to the element, instead of having it exist in an external file and cause trouble with asset garbage collection (can I remove this rule or is it still used anywhere?) External files still exist, though, for CSS rules that are not limited to a single template.
Bonus : the triple hash
How do I define some code that should be called when a button is clicked? Defining it directly in the onclick method is ugly, hard to read and does not let the application provide parameters, so what else can I do?
The solution is to use an intermediary global object that happens to be the same for the entire file — a pattern that stores any template-related JS in a global variable named after __FILE__ !
Yes, it is a hack, but it’s a simple and useful one.
The only difference is that __FILE__ is spelled ###.
<button type="button" onclick="###.frobnicate()">{t:frobnicate}</button>
<script type="text/coffeescript" params="message">
###.frobnicate = ->
alert message
</script>
Article Image © gdbg12 — Flickr




Hi. I'm Victor Nicollet,
Recent Comments