Skip to content

pimbrouwers/Falco.Htmx

Repository files navigation

Falco.Htmx

NuGet Version build

open Falco.Markup
open Falco.Htmx

let demo =
    Elem.button
        [ Hx.get "/click-me"
          Hx.swapOuterHtml
          Hx.targetCss "#wrapper" ]
        [ Text.raw "Reset" ]

Falco.Htmx brings type-safe htmx support to Falco. It provides a complete mapping of all attributes, typed request data and ready-made response modifiers.

Key Features

  • Idiomatic mapping of htmx attributes (i.e., hx-get, hx-post, hx-target etc.).
  • Typed access to htmx request headers.
  • Prepared response modifiers for common use-cases (i.e., HX-location, HX-Push-Url).

Design Goals

  • Create a self-documenting way to integrate htmx into Falco applications.
  • Match the specification of htmx as closely as possible, ideally one-to-one.
  • Provide type safety without over-abstracting.

Getting Started

This guide assumes you have a Falco project setup. If you don't, you can create a new Falco project using the following commands. The full code for this guide can be found in the Hello World example.

> dotnet new web -lang F# -o HelloWorld
> cd HelloWorldApp

Install the nuget package:

> dotnet add package Falco
> dotnet add package Falco.Htmx

Remove any *.fs files created automatically, create a new file named Program.fs and set the contents to the following:

open Falco
open Falco.Htmx
open Falco.Markup
open Falco.Routing
open Microsoft.AspNetCore.Builder

let bldr = WebApplication.CreateBuilder()
let wapp = bldr.Build()

let endpoints =
    [
    ]

wapp.UseRouting()
    .UseFalco(endpoints)
    .Run()

Now, let's incorporate htmx into our Falco application. First we'll define a simple route that returns a button that, when clicked, will swap the inner HTML of a target element with the response from a GET request.

let handleIndex : HttpHandler =
    let html =
        Elem.html [] [
            Elem.head [] [
                Elem.script [ Attr.src HtmxScript.cdnSrc ] [] ]
            Elem.body [] [
                Text.h1 "Example: Hello World"
                Elem.button
                    [ Hx.get "/click"
                      Hx.swapOuterHtml ]
                    [ Text.raw "Click Me" ] ] ]

    Response.ofHtml html

Next, we'll define a handler for the click event that will return HTML from the server to replace the outer HTML of the button.

let handleClick : HttpHandler =
    let html =
        Text.h2 "Hello, World from the Server!"

    Response.ofHtml html

And lastly, we'll make Falco aware of these routes by adding them to the endpoints list.

let endpoints =
    [
        get "/" handleIndex
        get "/click" handleClick
    ]

Save the file and run the application:

> dotnet run

Navigate to https://localhost:5001 in your browser and click the button. You should see the text "Hello, World from the Server!" appear in place of the button.

htmx Attributes

hx-[get|post|put|patch|delete]

Elem.button [ Hx.put "/messages" ] [
    Text.raw "Put to Messages" ]

hx-trigger

Elem.div [ Hx.post "/mouse-enter"; Hx.trigger "mouseenter" ] [
    Text.raw "Here mouse, mouse!" ]

// Trigger modifiers
Elem.div [ Hx.post "/mouse-enter"; Hx.trigger ("mouseenter", [HxTrigger.Once]) ] [
    Text.raw "Here mouse, mouse!" ]

// Trigger filters
Elem.div [ Hx.post "/mouse-enter"; Hx.trigger ("mouseenter", [HxTrigger.Once], "ctrlKey") ] [
    Text.raw "Here mouse, mouse!" ]

hx-target

Elem.form [] [
    Elem.input [ Hx.get "/search"; Hx.target "#search-results" ]
]
Elem.div [ Attr.id "search-results" ] []

hx-swap

Elem.button [ Hx.post "/like"; Hx.swapOuterHtml ] [
    Text.raw "Like" ]

hx-swap-oob

Elem.div [ Attr.id "message"; Hx.swapOobOn ] [
    Text.raw "Swap me directly" ]

// Equivalent to:
Elem.div [ Attr.id "message"; Hx.swapOob HxSwap.OuterHTML ] [
    Text.raw "Swap me directly" ]

// With a selector:
Elem.div [ Attr.id "message"; Hx.swapOob (HxSwap.InnerHTML, "#falco") ] [
    Text.raw "Swap me directly" ]

hx-select

Elem.button [ Hx.get "/info"; Hx.select "#info-detail"; Hx.swapOuterHtml ] [
    Text.raw "Get Info" ]

hx-select-oob

Elem.div [ Attr.id "alert" ] []
Elem.button [ Hx.get "/info"; Hx.select "#info-detail"; Hx.swapOuterHtml; Hx.selectOob "#alert" ] [
    Text.raw "Get Info" ]

hx-boost

Elem.div [ Hx.boostOn ] [
    Elem.a [ Attr.href "/blog" ] [ Text.raw "Blog" ] ]

hx-push-url

Elem.div [ Hx.get "/account"; Hx.pushUrl true ] [
    Text.raw "Go to My Account" ]

// Or short form:
Elem.div [ Hx.get "/account"; Hx.pushUrlOn ] [
    Text.raw "Go to My Account" ]

// Or specify URL:
Elem.div [ Hx.get "/account"; Hx.pushUrl "/my-account" ] [
    Text.raw "Go to My Account" ]

hx-sync

Elem.form [ Hx.post "/store" ] [
    Elem.input [ Attr.name "title"; Hx.post "/validate"; Hx.trigger "change"; Hx.sync ("form", HxSync.Abort) ] ]

hx-include

Elem.button [ Hx.post "/register"; Hx.includeCss "[name=email]" ] [
    Text.raw "Register!" ]
Elem.span [] [
    Text.raw "Enter email: "
    Elem.input [ Attr.name "email"; Attr.type' "email" ] [] ]

// Hx.includeCss "[name=email]" is equivalent to:
Elem.button [ Hx.post "/register"; Hx.include' (HxTarget.Css "[name=email]") ] [
    Text.raw "Register!" ]
Elem.span [] [
    Text.raw "Enter email: "
    Elem.input [ Attr.name "email"; Attr.type' "email" ] [] ]

hx-params

Elem.div [ Hx.get "/example"; Hx.params "*" ] [
    Text.raw "Get Some HTML, Including Params" ]

hx-vals

Elem.div [ Hx.get "/example"; Hx.vals """{"myVal": "My Value"}""" ] [
    Text.raw "Get Some HTML, Including A Value in the Request" ]

// Or with a dynamic value:
Elem.div [ Hx.get "/example"; Hx.vals "js:{myVal: calculateValue()}" ] [
    Text.raw "Get Some HTML, Including a Dynamic Value from Javascript in the Request" ]

hx-confirm

Elem.button [ Hx.delete "/account"; Hx.confirm "Are you sure you wish to delete your account?" ] [
    Text.raw "Delete My Account" ]

hx-disable

Elem.div [ Hx.disable ] []

hx-disabled-elt

Elem.button [ Hx.post "/example"; Hx.disabledThis ] [
    Text.raw "Post It!" ]

// Equivalent to:
Elem.button [ Hx.post "/example"; Hx.disabled HxTarget.This ] [
    Text.raw "Post It!" ]

hx-inherit

Elem.div [ Hx.targerCss "#tab-container"; Hx.inherit' "hx-target" ] [
    Elem.a [ Hx.boostOn; Attr.href "/tab1" ] [ Text.raw "Tab 1" ]
    Elem.a [ Hx.boostOn; Attr.href "/tab2" ] [ Text.raw "Tab 2" ]
    Elem.a [ Hx.boostOn; Attr.href "/tab3" ] [ Text.raw "Tab 3" ] ]

hx-disinherit

Elem.div [ Hx.boostOn; Hx.select "#content"; Hx.targetCss "#content"; Hx.disinherit "hx-target" ] [
    Elem.button [ Hx.get "/test" ] [] ]

hx-encoding

Elem.form [ Hx.encodingMultipart ] [
    (* ... form controls ... *) ]

hx-ext

Elem.div [ Hx.ext "example" ] [
    Text.raw "Example extension is used in this part of the tree..."
    Elem.div [ Hx.ext "ignore:example" ] [
        Text.raw "... but it will not be used in this part." ] ]

hx-headers

Elem.div [ Hx.get "/example"; Hx.headers [ "myHeader", "My Value" ] ] [
    Text.raw "Get Some HTML, Including A Custom Header in the Request" ]

// Or to evaluate a dynamic value:
Elem.div [ Hx.get "/example"; Hx.headers ([ "myHeader", "calculateValue()" ], true) ] [
    Text.raw "Get Some HTML, Including A Custom Header in the Request" ]
// ^-- produces hx-headers='js:{"myHeader": calculateValue()}'

hx-history

Elem.div [ Hx.historyOff ] []

hx-history-elt

Elem.div [ Hx.historyElt ] []

hx-indicator

Elem.div [] [
    Elem.button [ Hx.post "/example"; Hx.indicator "#spinner" ] [
        Text.raw "Post It!" ]
    Elem.img [ Attr.id "spinner"; Attr.class' "htmx-indicator"; Attr.src "/img/bars.svg" ] ]

Kudos

Big thanks and kudos to @dpraimeyuu for their collaboration in starting this repo!

Find a bug?

There's an issue for that.

License

Built with ♥ by Pim Brouwers and Damian Plaza. Licensed under Apache License 2.0.