(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 have several reasons why this approach in unattractive to me, so I'll provide some feedback as requested. Before I do, though, let me just say that while I disagree pretty heartily with your premise(s), you've created a respectable solution if all of the assumptions you've made are actually true.
One of the main goals that I think should be foremost on the minds of template engine creators is re-use of identical templates and template engines in both client and server, to achieve better DRY coding.
Your write-up here assumes that the primary use of templating engines is client-side. I'm sure there are a lot of sites doing that, but I think (and always have) that it's an anti-pattern, even for SPA's, to build most of your presentation via JS in the browser. Instead, I think most of the main heavy lifting should be server-side (a la twitter's latest approach), and (unlike twitter's latest approach), having responsive code (no, NOT responsive design buzzword) on the client that can do limited client side templating (rendering) for in-page updates. The ideal goal would be 80/20 split between server and client, maybe even 90/10.
In that respect, the problem with your approach is that to build my page on the server using the same "template" code above that I'd use on the client side, I'd have to use a server-DOM-shim, which is sucks for performance because it has to build it as a DOM in memory, then serialize that DOM down to a string, then send the string over the wire, then reconstitute the DOM from that string, thereby entirely eliminating any possible benefit of your stated approach's goals.
The example given above is a little bit disingenuous, if it really is meant to be something that would run on the client side, because it's extremely rare to see a site that regenerates the entire
<html>
element and downward, including even a new<!DOCTYPE ...>
, new<head>
, etc. Almost all SPA's simply replace the<body>
, or even more common, just a<div>
inside the body. It's a little unclear from your example how you'd use your syntax to regenerate only part of the document, but I'm assuming thatDIV(..)
would return you an actual DOM element, and then you'd use normal DOM manipulation methods (outside of your syntax, I'm assuming) to replace it in the correct location.What SPA's do commonly do is ADD new
<script>
or<link rel="stylesheet">
resources to the<head>
when they load up new pages that require stuff which hasn't been previously used. So, for them, they'd almost certainly NOT want to replace the entire<head>
just to do that, so they'd probably not use this DSL syntax at all for those tasks (and instead use established DOM manip logic), which further reduces the utility of your syntax for the most common use cases.I frankly would find it more confusing to have 2 (or more) different types of JS code approaches for page building, sometimes using this syntax, and sometimes doing it more manually with old-school DOM logic. I want less things to remember/understand, not more.
This sounds more like a liability, or at best a moot point, to me. A proper templating engine can and will provide good useful debug messages during compilation, and will ALSO provide error handling for run-time template rendering. For production usage, you generally don't want any errors being thrown, so this is all stuff you'd only be using when in development/debug mode.
When debugging, I see no benefit to having the error reference a line of JS with
DIV(...)
as the call, as opposed to an friendly debug message that says "line 5, character 3" referring to the original template source. Saying you prefer the more unfriendly error message to reference your "compiled" JS (because you are certainly doing minification on your client-side JS, right?!?) is like saying you prefer those error messages from compiled CS (as JS) instead of having a nice SourceMap mapping back to the original CS source. I don't understand at all this point.You seem to have a heavy assumption here that the primary skillset of the person building the HTML of a site/app is a JS developer. I find that to be quite a bit not-true in most of the dev teams and projects I've worked on. I think a lot of the good templating engines aim to provide a less-programmer-heavy approach to building HTML. Otherwise, why not just have JS devs build up their pages in JS strings like we did in 2001?
Also, why do we hate HTML? There's so much implicit dislike for the markup language in stuff like this, even in the embrace of things like HAML and Jade, like there must be so much that's wrong with how HTML works that we need anything BUT HTML to build up our pages.
What's wrong with having an HTML markup expert primarily work in just HTML, with a few extra presentational-logic conventions from the templating syntax thrown in to help them be more effective? Why force your HTML guy to start working entirely in JS?
And a similar argument goes for CSS? If you have a CSS expert, why force them to start working mostly in JS? If they like JS already, that's great, but if they don't (and many don't), you're shoving them into a foreign land.
Given a dev team where specialties are split out among team members, I prefer my markup guy to work in markup, my CSS guy to work in CSS, and my JS guy to work in JS. Right tool, right job.
Again, I think there's an incorrect assumption here, which is that most sites use client-side templating by sending their templates up to the client, and doing the compilation there. Most responsible sites compile their templates server-side, and send the compiled template (JS) code up to the clients, which only does the rendering client side. Suggesting that most sites primarily compile templates in the browser is like suggesting that most sites which use CS do so by shipping the CS code to the browser and compiling it there. That would be, in almost all cases, a nonsense approach.
There really are just so few cases for compiling templates in the browser (but certainly are plenty for rendering them there), I don't see how this point is anything more than a strawman. Your "Instead of ..." statement is more accurately "Instead of having pre-compiled JS functions that produce HTML that is turned into the DOM, have pre-written JS functions that produce DOM directly." When framed like that, you're actually making an accurate representation of the differences in approaches. And, I think fairly, there's far less of a relevant difference than in what you originally stated (which involved the "compilation in the browser" stuff).
Have you compared the performance characteristics of this approach to traditional pages, which put CSS and JS in separate files, allowing them to load in parallel, and to separately cache? I suspect the performance implications of this approach to be quite a bit worse, because you're de-emphasizing using a separate file or files for your CSS, and instead essentially inlining all your CSS into your JS. This means that your CSS and your JS are inexorably tied to each other in terms of both initial load, and subsequent cache-or-reload behavior, which neuters some of the power of that part of the pipeline.
Also, I suspect that minification (and gzip) of this kind of JS will not produce the same reduction in code size as if you used a separate CSS file that could be minified and gzip'd independently. I could be wrong on that, so I'm just curious if you've done any testing/research on the impact of your approach to these sorts of things?
Lastly, by moving all your markup and CSS into your JS, you've neutered a whole slew of validation and optimization tools in the ecosystem that were designed around the markup and CSS paradigms, respectively. Markup validators and CSS Linters will all have to be tossed out, or radically re-trained to instead look at this franken-JS mashup syntax.
I hope this feedback is taken as helpful and constructive criticism and not an attack.