open Falco.Markup
open Falco.Htmx
let demo =
_button
[ Hx.get "/click-me"
Hx.swapOuterHtml
Hx.targetCss "#wrapper" ]
[ _text "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.
- Idiomatic mapping of
htmxattributes (i.e.,hx-get,hx-post,hx-targetetc.). - Typed access to htmx request headers.
- Prepared response modifiers for common use-cases (i.e.,
HX-location,HX-Push-Url). - Built-in support for template fragments.
- 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.
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 HelloWorldAppInstall the nuget package:
> dotnet add package Falco
> dotnet add package Falco.HtmxRemove 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 =
_html [] [
_head [] [
_script [ _src_ HtmxScript.cdnSrc ] [] ]
_body [] [
_h1' "Example: Hello World"
_button
[ Hx.get "/click"
Hx.swapOuterHtml ]
[ _text "Click Me" ] ] ]
Response.ofHtml htmlNext, 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 =
_h2' "Hello, World from the Server!"
Response.ofHtml htmlAnd 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 runNavigate 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.
_button [ Hx.put "/messages" ] [
_text "Put to Messages" ]_div [ Hx.post "/mouse-enter"; Hx.trigger "mouseenter" ] [
_text "Here mouse, mouse!" ]
// Trigger modifiers
_div [ Hx.post "/mouse-enter"; Hx.trigger ("mouseenter", [HxTrigger.Once]) ] [
_text "Here mouse, mouse!" ]
// Trigger filters
_div [ Hx.post "/mouse-enter"; Hx.trigger ("mouseenter", [HxTrigger.Once], "ctrlKey") ] [
_text "Here mouse, mouse!" ]_form [] [
_input [ Hx.get "/search"; Hx.target "#search-results" ]
]
_div [ _id_ "search-results" ] []_button [ Hx.post "/like"; Hx.swapOuterHtml ] [
_text "Like" ]_div [ _id_ "message"; Hx.swapOobOn ] [
_text "Swap me directly" ]
// Equivalent to:
_div [ _id_ "message"; Hx.swapOob HxSwap.OuterHTML ] [
_text "Swap me directly" ]
// With a selector:
_div [ _id_ "message"; Hx.swapOob (HxSwap.InnerHTML, "#falco") ] [
_text "Swap me directly" ]_button [ Hx.get "/info"; Hx.select "#info-detail"; Hx.swapOuterHtml ] [
_text "Get Info" ]_div [ _id_ "alert" ] []
_button [ Hx.get "/info"; Hx.select "#info-detail"; Hx.swapOuterHtml; Hx.selectOob "#alert" ] [
_text "Get Info" ]_div [ Hx.boostOn ] [
_a [ _href_ "/blog" ] [ _text "Blog" ] ]_div [ Hx.get "/account"; Hx.pushUrl true ] [
_text "Go to My Account" ]
// Or short form:
_div [ Hx.get "/account"; Hx.pushUrlOn ] [
_text "Go to My Account" ]
// Or specify URL:
_div [ Hx.get "/account"; Hx.pushUrl "/my-account" ] [
_text "Go to My Account" ]_form [ Hx.post "/store" ] [
_input [ _name_ "title"; Hx.post "/validate"; Hx.trigger "change"; Hx.sync ("form", HxSync.Abort) ] ]_button [ Hx.post "/register"; Hx.includeCss "[name=email]" ] [
_text "Register!" ]
_span [] [
_text "Enter email: "
_input [ _name_ "email"; _type_ "email" ] [] ]
// Hx.includeCss "[name=email]" is equivalent to:
_button [ Hx.post "/register"; Hx.include' (HxTarget.Css "[name=email]") ] [
_text "Register!" ]
_span [] [
_text "Enter email: "
_input [ _name_ "email"; _type_ "email" ] [] ]_div [ Hx.get "/example"; Hx.params "*" ] [
_text "Get Some HTML, Including Params" ]_div [ Hx.get "/example"; Hx.vals """{"myVal": "My Value"}""" ] [
_text "Get Some HTML, Including A Value in the Request" ]
// Or with a dynamic value:
_div [ Hx.get "/example"; Hx.vals "js:{myVal: calculateValue()}" ] [
_text "Get Some HTML, Including a Dynamic Value from Javascript in the Request" ]_button [ Hx.delete "/account"; Hx.confirm "Are you sure you wish to delete your account?" ] [
_text "Delete My Account" ]_div [ Hx.disable ] []_button [ Hx.post "/example"; Hx.disabledThis ] [
_text "Post It!" ]
// Equivalent to:
_button [ Hx.post "/example"; Hx.disabled HxTarget.This ] [
_text "Post It!" ]_div [ Hx.targerCss "#tab-container"; Hx.inherit' "hx-target" ] [
_a [ Hx.boostOn; _href_ "/tab1" ] [ _text "Tab 1" ]
_a [ Hx.boostOn; _href_ "/tab2" ] [ _text "Tab 2" ]
_a [ Hx.boostOn; _href_ "/tab3" ] [ _text "Tab 3" ] ]_div [ Hx.boostOn; Hx.select "#content"; Hx.targetCss "#content"; Hx.disinherit "hx-target" ] [
_button [ Hx.get "/test" ] [] ]_form [ Hx.encodingMultipart ] [
(* ... form controls ... *) ]_div [ Hx.ext "example" ] [
_text "Example extension is used in this part of the tree..."
_div [ Hx.ext "ignore:example" ] [
_text "... but it will not be used in this part." ] ]_div [ Hx.get "/example"; Hx.headers [ "myHeader", "My Value" ] ] [
_text "Get Some HTML, Including A Custom Header in the Request" ]
// Or to evaluate a dynamic value:
_div [ Hx.get "/example"; Hx.headers ([ "myHeader", "calculateValue()" ], true) ] [
_text "Get Some HTML, Including A Custom Header in the Request" ]
// ^-- produces hx-headers='js:{"myHeader": calculateValue()}'_div [ Hx.historyOff ] []_div [ Hx.historyElt ] []_div [] [
_button [ Hx.post "/example"; Hx.indicator "#spinner" ] [
_text "Post It!" ]
_img [ _id_ "spinner"; _class_ "htmx-indicator"; _src_ "/img/bars.svg" ] ]Response.withHxLocation "/new-location"
>> Response.ofHtml (_h1' "HX-Location")
// this is equivalent to:
Response.withHxLocationOptions ("/new-location", None)
>> Response.ofHtml (_h1' "HX-Location")
// with context:
let ctx = HxLocation(event = "click", source = HxTarget.This)
Response.withHxLocationOptions ("/new-location", Some ctx)
>> Response.ofHtml (_h1' "HX-Location")Response.withHxPushUrl "/new-push-url"
>> Response.ofHtml (_h1' "HX-Push-Url")Response.withHxRedirect "/redirect-url"
>> Response.ofHtml (_h1' "HX-Redirect")Response.withHxRefresh
>> Response.ofHtml (_h1' "HX-Refresh")Response.withHxReplaceUrl "/replace-url"
>> Response.ofHtml (_h1' "HX-Replace-Url")Response.withHxReswap HxSwap.InnerHTML
>> Response.ofHtml (_h1' "HX-Reswap")
// with selector:
Response.withHxReswap (HxSwap.OuterHTML, "#falco")
>> Response.ofHtml (_h1' "HX-Reswap")Response.withHxRetarget HxTarget.This
>> Response.ofHtml (_h1' "HX-Retarget")
// with selector:
Response.withHxRetarget (HxTarget.Css "#falco")
>> Response.ofHtml (_h1' "HX-Retarget")Response.withHxTrigger (HxTriggerResponse.Events [ "myEvent" ])
>> Response.ofHtml (_h1' "HX-Trigger")
// or with detailed events (content is serialized to JSON):
Response.withHxTrigger (HxTriggerResponse.DetailedEvents [ ("myEvent", {| someData = 123 |}) ])
>> Response.ofHtml (_h1' "HX-Trigger")Response.withHxTrigger (HxTriggerResponse.Events [ "myEvent" ])
>> Response.withHxTriggerAfterSettle
>> Response.ofHtml (_h1' "HX-Trigger-After-Settle")Response.withHxTrigger (HxTriggerResponse.Events [ "myEvent" ])
>> Response.withHxTriggerAfterSwap
>> Response.ofHtml (_h1' "HX-Trigger-After-Swap")Response.withHxReselect "#falco"
>> Response.ofHtml (_h1' "HX-Reselect")Falco.Htmx has built-in support for template fragments. This allows you to return only a fragment of a larger HTML document in response to an htmx request, without having to create separate template function for each fragment.
This is supported by the Response.ofFragment function, the hx-swap attribute and optionally the hx-select attribute.
For an overview, see the Click & Load example.
Big thanks and kudos to @dpraimeyuu for their collaboration in starting this repo!
There's an issue for that.
Licensed under Apache License 2.0.