A custom template implementation

The need

  • Textual description of the source content
  • Flag to know if it has a test counterpart
  • Optional flag to allow documentation extraction
  • Optional documentation flags that stress out the most important parts of the source (such as class implementation, main method name…)

Building HTML pages

DOM API

  • Fast
  • Full control on the generation
  • Can be debugged
  • Exhaustive but complex API
  • Takes more time to develop
  • Long code for simple output
  • Code is quite cryptic and hard to evolve
  • Not easily maintainable
var data = {
title: "Code sample",
id: "test",
checked: "checked",
label: "it works"
};
var h1 = document.body.appendChild(document.createElement("h1"));
h1.innerHTML = data.title;
var input = document.body.appendChild(document.createElement("input"));
input.setAttribute("id", data.id);
input.setAttribute("type", "checkbox");
input.setAttribute("checked", "");
var label = document.body.appendChild(document.createElement("label"));
label.setAttribute("for", data.id);
label.setAttribute("title", data.label);
label.innerHTML = data.label;

Template engine

  • Quite fast (depends on the engine)
  • Less code to develop
  • Easy to maintain
  • Rapid learning curve
  • Each engine has its conventions and API
  • Debugging
var html = Mustache.to_html(document.getElementById("tpl").innerHTML, {
title: "Mustache sample",
id: "test",
checked: "checked",
label: "it works"
});
document.body.appendChild(document.createElement("div")).innerHTML = html;
<script id="tpl" type="text/template">
<h1>{{title}}</h1>
<input id="{{id}}" type="checkbox" {{checked}}>
<label for="{{id}}" title="{{label}}">{{label}}</label>
</script>

Framework

  • Widget based frameworks (ExtJS, Open UI5…): each UI element is wrapped inside a control class. Building the interface can be done either through static descriptions (such as XML) or code.
  • HTML based frameworks (AngularJS, EmberJS…): based on HTML, it is then augmented with bindings
  • Codebase (samples, documentation…)
  • Application oriented (does more than templating)
  • Heavy
  • Long learning curve
  • May become a nightmare to debug if anything goes wrong
  • Design may look rigid
var myApp = angular.module('myApp',[]);myApp.controller('SampleController', ['$scope', function($scope) {
$scope.title = "Angular sample";
$scope.id="test";
$scope.checked=true;
$scope.label="it works";
}]);
<html ng-app="myApp">
<!-- ... -->
<body ng-controller="SampleController">
<h1>{{title}}</h1>
<input id="{{id}}" type="checkbox" ng-checked="checked">
<label for="{{id}}" title="{{label}}">{{label}}</label>
</body>
</html>

Building a simple template engine

  • Flexible and easy way to define valid HTML
  • Simple textual bindings
  • JavaScript injection
  • An object providing values for substitution
  • An index that will distinguish objects when used in an enumeration
  • {{fieldName}} to be replaced with the object’s field named fieldName: it can be used inside any textual content (attributes or textual nodes).
  • {% JAVASCRIPT CODE %} to inject JavaScript (with some limitations, see below)
  • $write() to output any HTML code (to be used carefully)
  • $object gives the current object
  • $index gives the current index

The checkbox case

<input type="checkbox" {% JAVASCRIPT CODE %}>
"<input type=\"checkbox\" {%=\"\" javascript=\"\" code=\"\" %}=\"\">"
<input type="checkbox" {%%}="JAVASCRIPT CODE">
"<input type=\"checkbox\" {%%}=\"JAVASCRIPT CODE\">"

The template tag

  • the programmatic approach maximizes the capacities when setting the configuration (environment detection, conditions, loops…) but with a cost in terms of maintenance, compatibility and migration
  • the declarative approach simplifies the file but also gives limits to what you can do with it
  • It is parsed but not rendered: it speeds up the loading of the page and no special handling is required to hide it
  • It accepts any HTML content: try setting innerHTML to “<tr></tr>” on a DIV element, it won’t accept it.
<body>
<template id="tpl_row">
{%
function check(a, b) {
if ($object[a] && (b === undefined || $object[b])) {
$write("checked");
}
}
%}
<tr>
<td>{{name}}</td>
<td>{{description}}</td>
<td><input type="checkbox" {%%}="check('load');"></td>
<td><input type="checkbox" {%%}="check('load', 'test');"></td>
<td><input type="checkbox" {%%}="check('doc');"></td>
</tr>
</template>
</body>

Tokenizing

Code generation

<template id="sample1">This is a static one</template>
(function() {
var __a = arguments
, $object = __a[0]
, $index = __a[1]
, __r = []
, __d = document
, __t = __d.createElement("template");
function __s(v) {
if (undefined === v)
return "";
return v.toString();
}
function $write(t) {
__r.push(__s(t));
}
__r.push("This is a static one");
__t.innerHTML = __r.join("");
return __d.importNode(__t.content, true);
}
)
  • __a shortcut to arguments
  • __r the result HTML array (array is used to speed up concatenation)
  • __t a new template element (that will receive the result HTML code)
  • __s a method that converts its parameter into a string (required for $write and basic binding)
<template id="sample4"><h1>{{title}}</h1>{{content}}</template>
(function() {
var __a = arguments
, $object = __a[0]
, $index = __a[1]
, __r = []
, __d = document
, __t = __d.createElement("template");
function __s(v) {
if (undefined === v)
return "";
return v.toString();
}
function $write(t) {
__r.push(__s(t));
}
__r.push("<h1>;");
__r.push(__s($object.title));
__r.push("</h1>;");
__r.push(__s($object.content));
__t.innerHTML = __r.join("");
return __d.importNode(__t.content, true);
}
)
<template id="sample7"><input type="checkbox" {%%}="if ($object.check) $write('checked=\'true\'');"></template>
(function() {
var __a = arguments
, $object = __a[0]
, $index = __a[1]
, __r = []
, __d = document
, __t = __d.createElement("template");
function __s(v) {
if (undefined === v)
return "";
return v.toString();
}
function $write(t) {
__r.push(__s(t));
}
__r.push("<input type=\"checkbox\" ");
if ($object.check)
$write('checked=\'true\'');
__r.push(">");
__t.innerHTML = __r.join("");
return __d.importNode(__t.content, true);
}
)
<template id="sample8">{% if ($object.condition) { %}<span>{% $write("Hello"); %}</span>{% } else { %}<div></div>{% } %}</template>
(function() {
var __a = arguments
, $object = __a[0]
, $index = __a[1]
, __r = []
, __d = document
, __t = __d.createElement("template");
function __s(v) {
if (undefined === v)
return "";
return v.toString();
}
function $write(t) {
__r.push(__s(t));
}
if ($object.condition) {
__r.push("<span>");
$write("Hello");
__r.push("</span>");
} else {
__r.push("<div></div>");
}
__t.innerHTML = __r.join("");
return __d.importNode(__t.content, true);
}
)

Testing

Conclusion

  • error management (relate to the original template line if something wrong occurs when building the function)
  • two way bindings (I’d like to try that one…)
  • enumeration helpers (such as for each object property)
  • conditional helpers

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store