slaveOftime
fsharpGiraffeHTMXblazor

Mix Giraffe, HTMX and blazor together

2023-01-28
Source code repo is [here](https://github.com/albertwoo/GiraffeHtmxBlazor)

Giraffe is a very nice project for asp.net core to help you build rich routing for your web applications. Even it still does not support swagger, but for web views, it does not matter. And because its simplicity I can use it like this:

let uiRoutes: HttpHandler =
    choose [
        // For server side partial views
        subRouteCi "/view" (choose [
            routeCi "/empty" >=> cacheFor30Days >=> View.Build html.none
            routeCi "/products" >=> View.Build Products.Create 
            routeCif "/product-editor/%i" (fun id -> View.Build(ProductEditor.Create id))
            routeCi "/stocks" >=> View.Build (html.customElement<StocksView>(preRender = true))
        ])
        // For pages
        routeCi "/dashboard" >=> View.Build Dashboard.Create
        routeCi "/" >=> View.Build Index.Create
    ]
View.Build is just a helper in UI/Utils.fs

HTMX, for me, is a helper js library to cut your applications in multiple pieces and compose them together with RESTful APIs.

From my above code, the routes /view is just some partial views which can be loaded later with HTMX. And for pages routing, it is just simple server rendered complete html text.

Blazor is very powerful but very heavy. For blazor server you will always need to create a long live websocket which for some use cases is not necessary like some enterprise websites. If we use blazor webassembly, then the download size is too big and the first interactive is slow.

But it is very great toolkit for dotnet developer, and its custom-elements supported in dotnet 7 is very great to combine with plain html.

To use it we will have to register it,

services.AddServerSideBlazor(fun options ->
    options.RootComponents.RegisterCustomElementForFunBlazor<StocksView>()
)

Then, we can expose by partial views directly from the routing (I created pageckage Fun.CustomElements to make this cleaner):

routeCi "/stocks" >=> View.Build (html.customElement<StocksView>(preRender = true))

Or use it any places where your plain html rendered:

div {
    class' "my-5 rounded shadow-md bg-slate-100 p-5"
    html.customElement<StocksView>(preRenderNode = div {
        "Loading stocks ..."
        progress { class' "progress h-2" }
    })
}
div {
    hxGet "view/stocks"
    hxTarget "#lazy-stock-status"
    hxTrigger (HxTrigger(hxEvt.load, delayMs = 3_000))
    class' "my-5 rounded shadow-md bg-slate-100 p-5"
    childContent [
        ul { 
            li { "- display progress bar for 3s" }
            li { "- insert the prerendered stock status" }
            li { "- blazor will kick in by registered custom element" }
        }
        div {
            id "lazy-stock-status"
            "Loading stocks ..."
            progress { class' "progress h-2" }
        }
    ]
}
Above code is from *UI/Pages/Index.fs*

So the first div will use html custom element feature directly, it will have a pre-rendered information displayed. After everything is ready it will stream the stock information by blazor server.

For the second div it use HTMX to call the view/stocks API after the div is loaded in 3 seconds. and replace with the stock information by id lazy-stock-status. Those stock information will then later be streamed by blazor server custom element.

Below is the screenshots for different state:

First request:

Assets loaded and blazor's custom element rendered

HTMX triggered view/stocks

Dashboard pages explain...

I use HTMX to load product list partial views.

type Products =

    static member Create () =
        div.create [
            h2 { 
                class' "text-xl font-medium"
                "Prodution (Loaded async, powered by htmx)" 
            }
            for i in 1..5 do
                div {
                    hxGet $"view/product-editor/{i}"
                    hxTarget "#product-editor"
                    hxTrigger hxEvt.mouse.click
                    class' "px-5 py-1 my-1 cursor-pointer rounded bg-slate-100 hover:bg-slate-300"
                    $"product-{i}"
                }
            div { id "product-editor" }
        ]

When a product is clicked, I use HTMX to render the dialog. The dialog's close button is also powered by HTMX which will set the dialog parent's inner html to empty when it is clicked.

Below is the effect:

Below is the dashboard page code screenshot:

Summary

For me, this combination serves some use cases and has some advantages with Fun.Blazor.

First it is very clean, simple and consistency, below is the project structure:

Second, it is very light and SEO friendly. For home, blogs, about pages etc. It just serve some plain html with some lazy loading and simple interaction with HTMX.

3rd, it can be very powerful when necessary. For example, I have license management module, I can use html.customElement to render that. And this module, will have more complex interaction and hide the backend APIs by websocket.

Last week, I watched the video by Steve Sanderson. He mentioned that in dotnet 8 blazor may support component streaming and different render mode for pages which is great and exiting. And it can make my toolkit combination even simpler at that time. Looking forward to update my project Fun.Blazor and Fun.Htmx.

Do you like this post?