Since my last post
about Lucid, I’ve updated Lucid to major
version 2.0 in a way that removes the need for the with
combinator. Now, you can just write:
<children>
term_ <props>] <children> term_ [
Example:
page :: Html ()
=
page
html_do head_
(do title_ "Introduction page."
("stylesheet",type_ "text/css",href_ "screen.css"]
link_ [rel_ "body{background:red}")
style_
body_do div_ [id_ "header",style_ "color:white"] "Syntax"
("This is an example of Lucid syntax."))
p_ (span_ (strong_
hr_ []mapM_ (li_ . toHtml . show)
ul_ (1,2,3])
[do td_ "Hello!"
table_ (tr_ ("alt"] "World!"
td_ [class_ "Sup?")))) td_
Here’s the (pretty printed) output:
λ> page<!DOCTYPE html>
<html>
<head>
<title>Introduction page.</title>
<link href="screen.css" rel="stylesheet" type="text/css">
<style>body{background:red}</style>
</head>
<body>
<div id="header" style="color:white">Syntax</div>
<p><span><strong>This is an example of Lucid syntax.</strong></span></p>
<hr>
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<table>
<tr>
<td>Hello!</td>
<td class="alt">World!</td>
<td>Sup?</td>
</tr>
</table>
</body>
</html>
Element terms are now typed like this:
p_ :: Term arg result => arg -> result
Giving a couple overloaded instances:
p_ :: Monad m => [Attribute] -> HtmlT m () -> HtmlT m ()
p_ :: Monad m => HtmlT m () -> HtmlT m ()
This is similar to the variadic printf
from
Text.Printf
, but limited to one level of variance.
In my last post I listed a bunch of factors that Lucid should solve, I worked hard to make sure these were met in this change.
You can still use style_
or title_
as an
element or an attribute:
> style_ [style_ "inception"] "Go deeper." :: Html ()
λ<style style="inception">Go deeper.</style>
The script_
and style_
elements still
output unencoded:
> script_ "alert('Hello!' > 12)" :: Html ()
λ<script>alert('Hello!' > 12)</script>
You can still add attributes to elements using with
:
> p_ [id_ "foo"] "" :: Html ()
λ<p id="foo"></p>
> with (p_ [id_ "foo"]) [class_ "red"] "yo" :: Html ()
λ<p id="foo" class="red">yo</p>
You can construct custom elements if needed:
> with (term "potato" [id_ "foo"]) [class_ "red"] "yo" :: Html ()
λ<potato id="foo" class="red">yo</potato>
But you can also construct normal elements with a custom class, so
that you don’t have to use with
for extending elements like
our old container_
example, you can construct an element
with some given attributes:
> let container_ = termWith "div" [class_ " container "] λ
And then use it later like a normal element:
> container_ [class_ "main"] "OK, go!" :: Html ()
λ<div class=" container main">OK, go!</div>
Some basic Bootstrap terms are available in Lucid.Bootstrap.
I didn’t change anything about the monad itself. Just the combinators. So you can still use it as a transformer:
> runReader (renderTextT (html_ (body_ (do name <- lift ask
λ"name"] (toHtml name)))))
p_ [class_ "Chris" :: String)
("<html><body><p class=\"name\">Chris</p></body></html>"
One small difference is that elements that take no children always take arguments:
-- | @input@ element
input_ :: Monad m => [Attribute] -> HtmlT m ()
= with (makeElementNoEnd "input") input_
So you will always write:
<something>] input_ [
But in practice it seems that elements with no children almost always
take a number of attributes. Exceptions to that rule are
br_
and hr_
, but those are quite rare. So this
is a very happy trade-off, I feel. (See the ‘real examples’ at the end
of this post.)
Extending elements like this is straight-forward using our usual
with
combinator. Example, suppose you’re sick of writing
the classic input type="text"
, you can define a combinator
like this:
text_ :: Monad m => [Attribute] -> HtmlT m ()
= with (with (makeElementNoEnd "input") [type_ "text"]) text_
And now you can write:
> text_ []
λ<input type="text">
> text_ [class_ "foo"]
λ<input type="text" class="foo">
Due to the overloadedness, similar to the overloaded strings example:
> "foo > bar" :: Html ()
λ> bar foo
You have to use a type annotation in GHCi:
> p_ "foo" :: Html ()
λ<p>foo</p>
Otherwise you get
No instance for
(Term arg0 a0)
arising from a use ofit
Most Haskellers won’t care about this case, but for GHCi users it’s a slight regression. Also, in some local where declarations, you might need a type signature. For example, the following is OK:
people :: Html ()
= ul_ (mapM_ person ["Mary Smith","Dave Jones"])
people where person name = li_ name
Whereas in this case:
bigTable :: [[Int]] -> Html ()
= table_ (mapM_ row t)
bigTable t where row r = tr_ (mapM_ (td_ . toHtml . show) r)
It’s a little harder for GHC to infer this, so you add a type-signature:
bigTable :: [[Int]] -> Html ()
= table_ (mapM_ row t)
bigTable t where row :: [Int] -> Html ()
= tr_ (mapM_ (td_ . toHtml . show) r) row r
Not a big deal given the benefits, but something to be aware of.
In total, I’ve made this library almost perfect for my own tastes.
It’s concise, easy to read and edit (and auto-format), it lacks
namespace issues, it’s easy to make re-usable terms, and it’s fast
enough. The need for the with
combinator was the only wart
that naggled me over the past week, I knew I’d end up making some
change. I’ve also covered the trade-offs that come with this design
decision.
As far as I’m concerned, Lucid can rest at major version
2.*
for a long time now. I added some newfangled HTML5
elements (who knew main
was now an element?) and a
test suite. You can expect the only minor version bumps henceforth
to be bugfixes, regression tests, and more documentation.
For some real examples: