(tl;dr DOM builders like domo trump HTML templates on the client.)
Like all web developers, I've used a lot of template engines. Like most, I've also written a few of them, some of which even fit in a tweet.
The first open-source code I ever wrote was also one of the the first template engines for node.js, [a port][node-tmpl] of the mother of all JavaScript template engines, [John Resig][jresig]'s [micro-templates][tmpl]. Of course, these days you can't swing a dead cat without hitting a template engine; one in eight packages on npm ([2,220][npm templates] of 16,226 as of 10/19) involve templates.
John's implementation has since evolved and [lives on in Underscore.js][underscore], which means it's the default choice for templating in Backbone.js. And for a while, it's all I would ever use when building a client-side app.
But I can't really see the value in client-side HTML templates anymore.
I recently built a small (under 1k minizipped) DOM library called domo. All it really does is pull the semantics of CSS and HTML into the syntax of JavaScript, which means you can write your entire client app in one language. It looks like this:
// this code returns an HTML DOM element
HTML({lang: "en"},
HEAD(
TITLE("domo"),
STYLE({type: "text/css"},
CSS("#container", {backgroundColor: "#eee"})
)
),
BODY(
DIV({id: "container"},
"For more details about domo, see the source: ",
A({href: "//github.com/jed/domo/blob/master/domo.js"}, "View source")
)
)
)
It doesn't replace CSS or HTML at all; it just lets you write them both in JavaScript. So instead of relying on the arbitrary design choices of SASS/LESS or HAML/Mustache for making your CSS and HTML more dynamic, you rely only on the design choices of JavaScript, which while subjectively less arbitrary, are objectively more known.
I think there's so much win in this approach that I'm surprised it's not more popular. Or to turn it around, I'm surprised most web developers tolerate the usual workflow for templating:
- Write HTML templates written in some arbitrary frankenlanguage,
- put them in several DOM script tags with a non-standard type,
- compile it into a function that takes data, HTML-escapes it, and returns HTML, and
- use jQuery to turn the html string into a proper DOM element.
when all we really need is a friendlier way to wrap the overly imperative and verbose DOM API into JavaScript code that mimics the resulting DOM.
Sure, using a DOM builder like domo instead of both a CSS preprocessor and HTML template engine means you lose the superficial syntax of both CSS and HTML, but you still preserve the underlying semantics and gain the following in return:
-
Reduce your exposure to XSS attacks. You don't need to rely on a library to sanitize your rendered data because the browser does it for you automatically with
createTextNode
andsetAttribute
. -
Eliminate a separate compile step. With DOM builders, "compilation" is done in the same JavaScript process as your code, which means that any syntax errors are thrown with line numbers in the same JavaScript process.
-
Write your DOM code where it makes sense. If you're writing a view that renders a DOM node, you can write it directly within the rendering code for convenience, and then pull it out into its own file when the size of your app requires it. You don't need to let implentations drive architectural decisions.
-
Use JavaScript syntax everywhere. Instead of remembering which symbols map to which behaviors like looping or escaping or conditionals or negation, just use JavaScript.
-
Decouple your syntax sugar. Instead of choosing a template engine with the ideal syntax, just use JavaScript, and do your sweetening on a more reliable level. CoffeeScript can go a long way in [making DOM building code look like HAML][browserver], for example.
-
Reduce the number of moving parts. Instead of shipping HTML containing strings that compile into JavaScript functions that return HTML that's used to create DOM nodes, just use JavaScript to create DOM nodes, eliminating underscore/jQuery dependencies at the same time.
-
Reuse existing infrastructure. Any tools you use in your JavaScript workflow, such as minification or packaging, can now be used for your styles and markup too. You can even use something like [browserify][browserify] to easily discover all app dependencies and maintain code modularity.
-
Lessen the burden of context switching. Whether using JavaScript on both the client and server enables code reuse is debatable, but that it prevents the overhead of switching contexts between languages is less so. It may be subjective, but I think using one language everywhere reduces the congitive overhead for web app development.
As more and more logic moves to the browser, I really think this approach makes a lot of sense and would love to see more developers give it a try. I'd love to [hear your feedback][hn].
[node-tmpl]: https://github.com/jed/tmpl-node/commit/33cfc11f54b0e6c33b9de7589c62057dae82848b) [jresig]: http://ejohn.org/blog [tmpl]: http://ejohn.org/blog/javascript-micro-templating/ [underscore]: http://documentcloud.github.com/underscore/#template [npm templates]: https://encrypted.google.com/search?q=template+OR+templating+site:npmjs.org [browserver]: https://github.com/jed/browserver.org/blob/master/app.coffee [browserify]: https://github.com/substack/node-browserify [hn]: http://news.ycombinator.com/item?id=4672353
I've liked this way of creating contents since I first saw it in MochiKit, then discovered Dan Webb's standalone version.
Once you're able to generate HTML strings from the same code, another benefit is reusable components - I'm using a DOMBuilder for all output creation in newforms and since the Node.js version of DOMBuilder defaults to HTML output, I can use it in my Node projects and reuse Form objects on both sides, e.g. these are my Jade form rendering mixins for a CRM.
You can also do templating this way - I have a mode plugin for DOMBuilder which apes the features and implementation of Django's template language (primarily for template inheritance, which since Django has been my must-have feature for any template language), bonuses being that you don't need to write a parser (#lazyweb), new "templatetags" are just functions and you can use JavaScript functions to generate chunks of template instead of putting everything in template logic.
Examples:
This is also a gateway drug to using comma-first, as if you're just using a text editor it's the only way to stay sane with the kind of nested structures you end up writing, as you don't have to scan a ragged edge to find missing commas and they all line up nicely to indicate the nesting level.
I found that once you've been doing this for a while and you're used to dealing with the nesting, nested array representations of elements start to become bearable - I added a method to DOMBuilder support using nested arrays with its DOM and HTML output modes, but for Backbone.js projects at the moment, I'm liking Tim Caswell's dombuilder more than anything else, as it makes it so easy to pull out references to generated elements as you're defining the structure.