Essay - Published: 2023.12.22 | create | fsharp | giraffe | html | htmx | tech |
DISCLOSURE: If you buy through affiliate links, I may earn a small commission. (disclosures)
HTMX is a tiny JS library that gives your HTML superpowers - allowing it to act more like a modern interactive app than a crufty old enterprise page. It does this by allowing you to swap out only bits of a page rather than requiring a full reload to change the UI (this is what those fancy clientside frameworks like Svelte, React, Vue, etc are doing under the hood to make them feel "modern"). This essentially means that you can build modern apps without any clientside framework - just you and your server-side rendered HTML/X.
I've recently been doing a lot of server-side HTML rendering with F# (in fact that's what's currently running this site). In this post we'll explore how to integrate HTMX into F# / Giraffe server-side rendered pages to give them superpowers.
Q: How can you integrate HTMX with F# / Giraffe server-side rendered HTML pages?
In this post we'll be building a simple example app that utilizes HTMX for pagination. The paradigm we'll be using is "Interactive Islands" which allows us to sprinkle extra interactivity where it's necessary without it invading our entire application (this is a similar approach to Astro).

The example app does this:
page paramThis will provide a simple overview of how you can mix your normal HTML loads with Interactive Islands powered by HTMX.

The example app is composed of:
This example app is obvs overkill for this example but it provides a semi-realistic environment from which to build from. Diving into these technologies / paradigms is out of scope for this post so checkout the linked guides if interested.
I'll be sharing source code to highlight how things work but if you'd like the full project code you can get that (along with dozens of other projects) in my Example Project Repo, available to HAMINIONs subscribers.
Note: My F# project boilerplate CloudSeed has recently been ported to F# / Giraffe w server-side HTML/X as well if you want to start using it to build your own projects.
The first thing we gotta do is install HTMX. This is simple as it's just a tiny JS package served via CDN so we can simply include it in a script tag at the top of our page.
<script src="https://unpkg.com/htmx.org@1.9.9"></script>
Once that's in there, you're good to go with HTMX! Simple!

Now that we have HTMX available, let's build up our little HTML table.
The way I'm thinking of HTML/X Targets is Interactive Islands. We can think of each of these Islands as a unit of interactivity that's semi-self contained. HTMX calls this a "Target" and this seems similar to me to what we might call a Component in mainstream clientside frameworks.
The main difference I see between Targets / Islands and typical clientside Components is that a Target / Island is typically a larger, self-contained Component that itself may contain many subcomponents underneath it. This is because there's a bit of overhead involved with Target / Island creation.
So some examples:
This is my current mental model but I'm still playing around with this stuff so that may change in the future.
For our simple example page, we really only have two targets:
So how do we know what Target the page is trying to get from us?
The best way I've found to do this is to rely on the headers HTMX attaches to requests it sends:
By utilizing these headers we can uniquely identify what the page is trying to re-render and route that to the Target we have registered for it.
In F# / Giraffe we can use a simple helper function I built to parse this from the request. This helper:
For more on how F# / Giraffe routing / payloads work, take a look at:
let getTargetFromContext
(targetsStrEnum : StrEnum<'a>)
(ctx : HttpContext)
: 'a option
=
let hxRequest = ctx.TryGetRequestHeader "HX-Request"
if Option.isNone hxRequest
then None
else
ctx.TryGetRequestHeader "HX-Target"
|> Option.bind (
fun targetString -> targetsStrEnum.GetEnum(targetString)
)
Now that we know how to get which Target the page is trying to render, let's look at how we selectively render them.
This is our page entrypoint which will handle full and Target re-renders. Note that I am using the same entrypoint to render the page and all its sub targets. This is in keeping with the idea of Interactive Islands where we are really building a simple page that just happens to support partial re-renders of its islands.
This may look weird at first but I think leads to much simpler state and view management. When we think of "reactive" or "functional" views, it really doesn't get much more functional than rendering conditionally based on passed-in params from the URL (URL as state source-of-truth).
This page:
Note: It's a bit wasteful to render both the full page and table component if we may not use both but I thought it was easier to read for this example and F# server-side HTML rendering is fast so doesn't really matter.
type MainPageProps =
{
Target : MainPageTargets
Page : int
SentinelCount : int
}
let renderMainPageAsync
(serviceTree : SentinelServiceTree)
(props : MainPageProps)
=
async {
do!
seq {0 .. 5}
|> Seq.map (fun _ ->
sendCreateSentinelCommandAsync serviceTree)
|> Async.Sequential
|> Async.Ignore
let! sentinelResult =
sendGetSentinelsQueryAsync
serviceTree
{
Page = props.Page
Count = props.SentinelCount
}
let sentinels =
match sentinelResult with
| Ok s ->
s
|> Seq.toList
| Error s -> raise (System.SystemException("Failed to get Sentinels"))
let sentinelTable =
renderSentinelTableComponent
{
HasNext = true
Sentinels = sentinels
Page = props.Page
}
let template =
$"""
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://unpkg.com/htmx.org@1.9.9"></script>
<meta charset="UTF-8">
<title>Sentinels Example</title>
</head>
<body>
<main>
<h1>Sentinels Table</h1>
</main>
{ sentinelTable }
</body>
</html>
"""
return
match props.Target with
| MainPageTargets.SentinelTable -> sentinelTable
| MainPageTargets.FullPage -> template
}
So that's how we're routing from the HTMX request to the target we want. Now we are going to dive into rendering the HTML which actually has the HTMX controls embedded in it and sends those requests.
Here we have a few things:
For a deeper dive into raw string HTML rendering with F#, check out: Build a Simple Markdown Blog with F# / Giraffe.
type SentinelTableComponentProps =
{
HasNext : bool
Page : int
Sentinels : Sentinel list
}
let renderSentinelTablePagination
(props :
{|
HasNext : bool
Page : int
|})
: string
=
let sentinelTableId = mainPageTargetsStrEnum.GetString(MainPageTargets.SentinelTable)
let previousButton =
if props.Page < 1
then ""
else
$"""
<button
hx-get="/sentinels?page={props.Page - 1}"
hx-target="#{sentinelTableId}"
hx-push-url="true" >
Previous
</button>
"""
let nextButton =
if not props.HasNext
then ""
else
$"""
<button
hx-get="/sentinels?page={props.Page + 1}"
hx-target="#{sentinelTableId}"
hx-push-url="true" >
Next
</button>
"""
$"""
{ previousButton }
{ nextButton }
"""
let renderSentinelTableComponent
(props : SentinelTableComponentProps)
: string
=
let sentinelRows =
htmlForEach
props.Sentinels
(fun sentinel ->
$"""
<tr>
<td>{ sentinel.id }</td>
<td>{ sentinel.data.name }</td>
</tr>
"""
)
let sentinelTableId = mainPageTargetsStrEnum.GetString(MainPageTargets.SentinelTable)
$"""
<div id="{ sentinelTableId }">
<table>
<tr>
<th>ID</th>
<th>Data</th>
</tr>
{ sentinelRows }
</table>
%s{
renderSentinelTablePagination
{|
HasNext = props.HasNext
Page = props.Page
|}
}
<div>
Page: { props.Page }
</div>
</div>
"""
Okay so at this point we've walked through how to get HTMX on your page, rendering our HTML pages with embedded HTMX controls, and routing to the correct target based on HTMX header.
In this section I want to dive into how we're actually accomplishing Type-Safe targets with strings <> Enums because this was a bit of a faff to setup but I think is very important to leverage the full power of the F# type system. The reason this was hard is because F# does not natively support string Enums (something like the StrEnum in Python). This is not that surprising as C# does not do that either but it's a bit of a pain.
The reason this is a pain is that HTMX / HTML themselves are not very type-safe (they really just big strings). But it's absolutely critical to get these target ids and urls correct otherwise your whole page fails. This means most of these errors will be runtime errors (and thus relatively hard to catch unless you have tests covering them which usually means you find them in prod).
So we want to try and move at least some of the most important parts of HTMX into type-safe land so we can get build time errors instead of runtime errors.
For this I wanted to create a string enum that allows for easy parsing between:
This is how I did it:
let parseEnum<'T when 'T :> Enum> (value : string) =
System.Enum.Parse(typedefof<'T>, value, true) :?> 'T
type StrEnum<'TEnum when 'TEnum :> Enum>(enumToStringMap : 'a -> string) =
let getAllCases =
fun () ->
System.Enum.GetNames(typeof<'TEnum>)
let enumNameToStringLookup
=
getAllCases()
|> Array.map (
fun case ->
(case, (enumToStringMap (parseEnum<'TEnum> case)))
)
|> dict
let enumStringToNameLookup
=
getAllCases()
|> Array.map (
fun case ->
((enumToStringMap (parseEnum<'TEnum> case)), case)
)
|> dict
member __.GetEnum (s : string) : 'TEnum option =
if not (enumStringToNameLookup.ContainsKey(s))
then None
else
let enumName = enumStringToNameLookup[s]
Some (
parseEnum<'TEnum> enumName
)
member __.GetString (e : 'TEnum) : string =
enumNameToStringLookup[e.ToString()]
Using this to register the Targets for my page:
type MainPageTargets =
| FullPage = 0
| SentinelTable = 1
let mainPageTargetsStrEnum = StrEnum<MainPageTargets>(
fun e ->
match e with
| MainPageTargets.FullPage -> "full-page"
| MainPageTargets.SentinelTable -> "sentinel-table"
)
Then utilizing these registered targets and StrEnum to get the type-safe Target from the F# / Giraffe request. (we saw this earlier but copypasting again for clarity)
let getTargetFromContext
(targetsStrEnum : StrEnum<'a>)
(ctx : HttpContext)
: 'a option
=
let hxRequest = ctx.TryGetRequestHeader "HX-Request"
if Option.isNone hxRequest
then None
else
ctx.TryGetRequestHeader "HX-Target"
|> Option.bind (
fun targetString -> targetsStrEnum.GetEnum(targetString)
)
Hopefully this helps if you're looking to build type-safe targets as well. Also hopefully I didn't totally overcomplicate this.
Let me know if you have been personally victimized by F#'s lack of String Enum support or if you have suggestions for making this easier. I'd love to find a better solution than this.
That's it! A lil server-side rendered HTML page empowered by HTMX and built with F#.
Let me know if you have any Qs or if you have suggestions for doing this easier / better. I'm still learning / playing around with this stuff so I'm sure there's lots of ways to improve.
If you liked this post, you might also like:
If you're looking to build your own apps with F#, consider starting with CloudSeed - my F# project boilerplate.
The best way to support my work is to like / comment / share for the algorithm and subscribe for future updates.