Building truly integrated systems
Recently, the creator of htmx has been conducting the template fragments hype train and calling for programmers to expose whether or not template fragments are possible in their templating language of choice. If you’re not familiar with what template fragments are, read this essay. In short, they are fragments of an HTML template that can be used on their own without creating new, individual files for them. My templating language of choice is lucid: a monadic DSL for rendering HTML in Haskell. Let’s go over how lucid works and then see how we can apply the template fragments pattern with it.
Lucid provides us with an Html
monad that we can use to sequence and nest HTML tags, like so:
myHtml :: Html ()
myHtml = do
h1_ [class_ "text-xl"] "Bio"
div_ [class_ "info-box"] $ do
h2_ [] "Name"
p_ [] "Rashad"
h2_ [] "Location"
p_ [] "Earth"
h2_ [] "Likes"
ul_ [] $ do
li_ [] "Haskell"
li_ [] "htmx"
li_ [] "The color green"
It’s pretty straight forward.
You have a tag function, like h1_
, that you apply to a list of attributes and an inner HTML value of type Html ()
.
If two tags have the same indentation level in a do
block, they will be rendered as siblings.
If a tag is within the do
block of the another tag’s inner HTML value, it will be rendered as a child of the other tag.
The above isn’t a template though, because everything is statically defined. We’re just writing HTML using a fancy syntax.
Let’s parameterize our myHtml
value by turning it into a function. We’ll call the template function personHtml
:
data Person = Person
{ name :: Text
, location :: Text
, likes :: [Text]
}
personHtml :: Person -> Html ()
personHtml p = do
h1_ [class_ "text-xl"] "Bio"
div_ [class_ "info-box"] $ do
h2_ [] "Name"
p_ [] $ toHtml p.name
h2_ [] "Location"
p_ [] $ toHtml p.location
h2_ [] "Likes"
ul_ [] $ mapM_ (li_ [] . toHtml) p.likes -- mapM_ is like the map function, but it works in a monadic context
Note
You probably noticed in the templating function that we’re using the
toHtml
function, but in the static version of the template we were able to use a string literal like"The color green"
without usingtoHtml
. This is because of how Haskell infers the types of string literals when theOverloadedStrings
language extension is enabled. When theOverloadedStrings
extension is on, GHC (the standard Haskell compiler) infers the string literal"The color green"
to be of typeHtml ()
automatically. GHC can’t do this in thepersonHtml
template function because the fields of thePerson
record are defined as being of typeText
. We could define thePerson
type asdata Person = Person { name :: Html () , location :: Html () , likes :: [Html ()] }
and remove the need to use
toHtml
in our template, but that would be unweildy to other parts of the program that need to manipulate those fields. It’s more practical if the fields are of typeText
, and we convert them toHtml ()
when it’s necessary.
Awesome! Now we have an HTML template that we can apply to any value of the type Person
:
myHtml :: Html ()
myHtml = personHtml $ Person
{ name = "Rashad"
, location = "Earth"
, likes = ["Haskell", "htmx", "The color green"]
}
bobHtml :: Html ()
bobHtml = personHtml $ Person
{ name = "Bob"
, location = "Antarctica"
, likes = ["The blues", "A good hamburger", "Swimming"]
}
Let’s use what we learned and implement the template fragments pattern. I’m going to use the example used in the original essay so we can compare and contrast between the approaches that these two templating libraries take.
First, let’s translate the first chill template used in the essay into lucid:
data Contact = Contact
{ id :: Int
, email :: Text
, archived :: Bool
}
contactDetail :: Contact -> Html ()
contactDetail contact = do
html_ [] $ do
body_ [] $ do
div_ [hxTarget_ "this"] $ do
if contact.archived
then button_ [hxPatch_ $ "/contacts/" <> toText contact.id <> "/unarchive"] "Unarchive"
else button_ [hxPatch_ $ "/contacts/" <> toText contact.id] "Archive"
h3_ [] "Contact"
p_ [] $ toHtml contact.email
Our goal is to turn the button in the contactDetail
template into its own template so that we can render it by itself if needed.
To do this, we can simply factor out the HTML we want to reuse into its own function, and use it like any other template function:
data Contact = Contact
{ id :: Int
, email :: Text
, archived :: Bool
}
contactDetail :: Contact -> Html ()
contactDetail contact = do
html_ [] $ do
body_ [] $ do
div_ [hxTarget_ "this"] $ contactArchiveUI contact
h3_ [] "Contact"
p_ [] $ toHtml contact.email
contactArchiveUI :: Contact -> Html ()
contactArchiveUI contact =
if contact.archived
then button_ [hxPatch_ $ "/contacts/" <> toText contact.id <> "/unarchive"] "Unarchive"
else button_ [hxPatch_ $ "/contacts/" <> toText contact.id] "Archive"
Now we can do the following:
someContact :: Contact
someContact = Contact
{ id = 101
, email = "someemail@some.com"
, archived = True
}
allTheDetails :: Html ()
allTheDetails = contactDetail someContact
justTheButton :: Html ()
justTheButton = contactArchiveUI someContact
There we go! Quite simple.
Lucid does support the concept of template fragments. You have to factor out the HTML you want to reuse into its own function and explicitly define its parameters, which is slightly less convinient than the chill templates example in the original essay. The chill template in the original essay doesn’t require you to factor out anything from the base template, but only annotate the fragment with an identifier that you use to refer to it. I also found it interesting that the chill template fragment seems to “inherit” the parameters passed into the base template.