(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
@jed I understand, the purpose of my feedback before, and here, is not to debate, but just to have a constructive discussion for clarification sake. Your comments definitely helped me understand better what your solution is going for, so that's a good thing. If nothing else, hopefully this helps you identify places where your solution can benefit from better "documentation". :)
Interesting thought. If your approach could either generate DOM or markup, what then would be the reason why you would use the DOM approach? If the concern is just avoiding having to set
innerHTML
because that sort of code is ugly, I'm sure whyinsertBefore(node,node.parent)
messiness is all that much better. Besides, your API could easily just do theinnerHTML
insertion for them, instead of DOM appending.And there have been a number of example performance tests done which, IIRC, show that in most cases
innerHTML
insertion is faster. Of course, it does have caveats of overriding event handlers, but that's something you have to be careful about no matter how you're modifying the page.So is there another benefit to DOM vs. markup insertion that I'm not seeing?
So, for cases where I did
DIV({id:"container"} ...)
, would it automatically replace that element under the same sort of assumption? That sounds nice, but then it creates more disparity if you doDIV({class:"foo"} ...)
and there are more than one of them in the page... does it replace all of them, or none of them (and force you to fall back to your own code to inject)?What I was getting at with the original point is, when you use a traditional string-based template engine, you always do the page insertion in the same way (inserting via
innerHTML
), so there's a consistency and predictability to that pattern. Your approach seems to suggest that there may end up being several (2 or more) different ways I have to modify the page depending on the type of element I'm dealing with.It would be nice if there didn't have to be very different approaches, or special rules of when it was automatic and when you had to do it yourself, etc.
Sorry, SPA = Single Page Application
I don't really use much by way of css pre-processors (not on that bandwagon, at least yet), but I do use my template engine grips for markup generation. I took great pains in making sure that it was very robust/friendly /helpful in its debug error messages, both during compilation and during run-time. It always shows you what symbol in the original template is the offender, and gives a line and column reference. This is true even when the "error" that happens is a run-time javascript exception in the compiled code.
If the template engines you're familiar with don't take this level of care for debugging utility, perhaps take a look at grips. :)
That's a fair point, but it brings up another concern that I have, a very strong one that was actually the primary motivator for me designing grips the way it is. I say in the docs that I want grips to be remarkable not for what it can do, but for what it cannot do. It's a restrained and simple DSL syntax for templating, which aims to give you only what tools you need, and not let you do the things that people often end up trying to squeeze into their template code which clearly violates SoC (separation of concerns).
SoC is so important to me, I had to go to the trouble of building a template engine that enforced it, because human/dev nature is so overriding that time and time again, in various teams and various dev cultures, if you can do something in a template, invariably some dev does do that in the template.
Example: it's really "nice and convenient" to format currency inside your template, so you just call out to some function (because your template engine can call functions and perform other general coding tasks). Then it's not too long before that formatting function is doing business logic (like checking a user's preferences in addition to their locale), and then it's not too long before you wrap your formatting call in some boolean operation combined conditional check, and ...
The point is, general coding syntax inside a template is the gateway drug to violation of SoC, and it has been one of the worst offenders in all my various dev experiences to poor app architecture, complicated code maintenance, etc.
Your approach throws that concern to the wind (you're definitely not the only "offender" -- many do!), and says basically, "hey, it's all code, intermingle at will, and we'll trust you to do the right thing..." :)
I don't think having the exact same template engine and templates, and letting an application decide, based on a variety of factors (including performance, capability of the connected user agent, etc), where to actually invoke the
render()
, is necessarily bad complexity. I think it's a flexibility and power that represents a next generation of "responsive" apps.What I would do, and what I often do, is have a general full-page template that's rendered on the server, and if there are parts or chunks of the page which must respond to user environment for their rendering, then those parts are simply left with empty placeholders, and then client side template rendering is invoked to generate the
innerHTML
for that placeholder. I'm not sure why that's seen as a bad pattern, it worked well on a number of my projects. But anyway, that's how I'd handle it.Sure, that's one valid approach, but it wasn't hinted at in your example, and instead the rather anti-pattern of "eschew the external stylesheet and drop all your CSS into an inline
<style>
tag" is what you showed, so that's what I was objecting to.Of course, to do that, you'd need for the "templating language" in question to be able to render out text for the CSS from the server (like you mentioned above with a
toHTML()
), and we get back to the above question of why we'd need both the capability to render CSS as DOM and as HTML? What would rendering CSS as DOM really buy us?The "rendered CSS" in your above example is never in a string form suitable for linting/validation, it's directly injected into the DOM, so it's never in a form that would be easily lintable by current tools.
If it was instead compiled/built into a string CSS representation, in a regular CSS file, on the server, during a build-step, then yes you definitely would be able to lint/validate it, but then... haven't we come back to one of the main things you were trying to avoid, which is to have a build step associated with your page generation?
The much more likely scenario is that if people wanted to lint this kind of CSS and markup, they'd need separate tools that were built to understand and inspect the JS DSL you've created. That's not abjectly a bad thing, but it's definitely an extra "cost" to your approach as opposed to sticking with traditional separation of markup and CSS into their own files, which lets them more easily be validated by existing tools.