Enlive is a Clojure library for generating HTML that uses transformations instead of templates. Rather than starting with templates containing code which are then executed to produce the final output, it starts with plain HTML which is subjected to a series of transformations; the transformations are ordinary functions, targeted to the right part of the DOM by standard CSS selectors.
This approach allows a clean separation between template and code; it avoids creating a novel hybrid language—often crippled and always nasty—for the templates.
As you can tell, I am pretty sold on the benefits of selector-based templating, so I was excited to come across Enlive when I started playing with Clojure. Enlive is powerful and somewhat complex; there are two good tutorials but I found it hard to get straight in my head which bits were really necessary to do something simple.
So, having wrestled with this for a while (and having surface after a longish dive in the Enlive source code), I’ve put together a basic example, which I think takes a reasonable, minimal approach to using Enlive for a simple web application. This post walks through it, explaining how it fits together. It is not intended to be a complete introduction to what Enlive can do; for that, read the tutorials and documentation.
When using Enlive, it’s helpful to know that the library generally uses an internal data structure representation of HTML (called nodes in the documentation). Most functions and macros operate on and return nodes, or collections of them; a few return strings, which you need in order to render the HTML to the outside world (in my case when defining Compojure routes).
The simplest thing we can do is read in an HTML file from disk with
html-resource
.
1
|
|
This returns nodes, which can be rendered into strings with emit*
.
1 2 |
|
We would also like to be able to show dynamic content. With a
transformation-based approach, we start with static HTML and apply
transformations to it. We’re going to create a <ul>
, with one <li>
for each item in a list. The initial HTML shows the structure we want,
with dummy content in the <li>
.
1 2 3 |
|
Again we read in the HTML using html-resource
, but this time we use
at
to apply a transformation.
1 2 3 |
|
This targets the <li>
element and applies a transformation which,
for every element in the argument list things
, clones the <li>
and
fills in its content.
Like html-resource
, at
returns nodes, so we can render the
result with emit*
.
1 2 3 4 5 |
|
(Enlive has a defsnippet
macro which we could use here, but it
doesn’t work in quite the way that we need.)
For a real web application, we need a way of applying common layout and styling to the pages. With Enlive we can use a further transformation to insert the pages that we have so far into a common piece of wrapper HTML.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Enlive provides a macro deftemplate
that we can use here. It wraps a
couple of the things we’ve seen already (at
and html-resource
) as
well as doing the function definition for us.
1 2 |
|
This replaces the contents of div.content
with the HTML that we pass
it. deftemplate
also handles the call to emit*
for us, so replace
it with layout
when defining our routes.
1 2 3 |
|
We can improve layout
to apply consistent titles to each page, by
adding an argument and setting the content of <title>
and <h1>
tags.
1 2 3 4 5 6 7 |
|
And we add those elements, with placeholders, to the layout HTML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
There is one wrinkle that I’ve glossed over up to this point. Enlive
uses the wonderful TagSoup to parse HTML (so that you can
use the same transformations on “wild” HTML that you’ve scraped from
the web and which may not be well-formed). Because TagSoup assumes
that it’s dealing with something potentially toxic, and tries to
detoxify it, it adds <head>
and <body>
tags around any HTML that
you give it. So the DOM returned by html-resource
has these extra
tags which end up wrapping the content in the middle of our webpage.
We need to modify layout
to strip these out.
Here is the complete code, which you can see in a working project here.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|