Oneiro.Giraffe.ViewEngine.Htmx
1.0.0
dotnet add package Oneiro.Giraffe.ViewEngine.Htmx --version 1.0.0
NuGet\Install-Package Oneiro.Giraffe.ViewEngine.Htmx -Version 1.0.0
<PackageReference Include="Oneiro.Giraffe.ViewEngine.Htmx" Version="1.0.0" />
<PackageVersion Include="Oneiro.Giraffe.ViewEngine.Htmx" Version="1.0.0" />
<PackageReference Include="Oneiro.Giraffe.ViewEngine.Htmx" />
paket add Oneiro.Giraffe.ViewEngine.Htmx --version 1.0.0
#r "nuget: Oneiro.Giraffe.ViewEngine.Htmx, 1.0.0"
#:package Oneiro.Giraffe.ViewEngine.Htmx@1.0.0
#addin nuget:?package=Oneiro.Giraffe.ViewEngine.Htmx&version=1.0.0
#tool nuget:?package=Oneiro.Giraffe.ViewEngine.Htmx&version=1.0.0
Oneiro.Giraffe.ViewEngine.Htmx
A comprehensive F# library that extends Giraffe.ViewEngine with type-safe HTMX attributes and handlers, enabling you to build modern, interactive web applications with minimal JavaScript.
Features
- 🔒 Type-safe HTMX attributes - Strongly-typed alternatives to string-based HTMX attributes
- 🚀 Smart HTTP handlers - Automatically adapt responses for HTMX vs full page requests
- 🎨 Layout builders - Fluent API for creating HTMX-enabled HTML layouts
- 📡 Request introspection - Easy access to HTMX request headers and context
- 🔧 Response control - Set HTMX response headers for client-side behavior
- 📖 Comprehensive documentation - Detailed XML docs for all public functions
Installation
dotnet add package Oneiro.Giraffe.ViewEngine.Htmx
Quick Start
1. Basic Setup
open Giraffe
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Htmx.Attributes
open Giraffe.ViewEngine.Htmx.Layouts
open Giraffe.ViewEngine.Htmx.Handlers
// Create an HTMX-enabled layout
let appLayout = htmxLayout {
title "My HTMX App"
version V2_0_6
styles [
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
"https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css"
]
}
// Create a partial handler that adapts to HTMX context
let homeHandler = htmx appLayout (fun () ->
div [ _class "container mt-4" ] [
h1 [] [ str "Welcome to HTMX with F#!" ]
p [] [ str "This content automatically adapts for HTMX requests." ]
])
2. Configure Your Giraffe App
let webApp =
choose [
GET >=> route "/" >=> homeHandler
// Add more routes here
]
[<EntryPoint>]
let main _ =
Host.CreateDefaultBuilder()
.ConfigureWebHostDefaults(fun webHostBuilder ->
webHostBuilder
.UseGiraffe(webApp)
|> ignore)
.Build()
.Run()
0
Core Concepts
HTMX Handlers
The htmx
handler automatically detects whether a request comes from HTMX or a full page load:
// For HTMX requests: returns only the content fragment
// For full page requests: wraps content in the provided layout
let myHandler = htmx layout (fun () ->
div [] [ str "Dynamic content" ]
)
// Using currying for reusable layouts
let withMainLayout = htmx appLayout
let homeHandler = withMainLayout (fun () -> homeView())
let aboutHandler = withMainLayout (fun () -> aboutView())
Type-Safe HTMX Attributes
Replace string-based HTMX attributes with strongly-typed alternatives:
// String-based (traditional)
button [ _hxPost "/api/users"; _hxTarget "#result" ] [ str "Create User" ]
// Type-safe alternative
button [
_hxPost "/api/users"
_hxTarget "#result"
_hxTriggerTyped (HtmxTrigger.click |> HtmxTrigger.once)
_hxSwapTyped (HtmxSwap.innerHTML |> HtmxSwap.withTransition)
] [ str "Create User" ]
Examples
Interactive Todo List
type Todo = { Id: int; Text: string; Completed: bool }
let mutable todos = [
{ Id = 1; Text = "Learn F#"; Completed = true }
{ Id = 2; Text = "Build HTMX app"; Completed = false }
{ Id = 3; Text = "Deploy to production"; Completed = false }
]
// Layout with custom styling
let todoLayout = htmxLayout {
title "F# HTMX Todo App"
styles [
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
]
head [
style [] [
rawText """
.todo-item { transition: all 0.3s ease; }
.todo-item.completed { opacity: 0.6; text-decoration: line-through; }
.htmx-indicator { opacity: 0; transition: opacity 0.3s; }
.htmx-request .htmx-indicator { opacity: 1; }
"""
]
]
}
// Todo item component
let todoItem todo =
div [
_class "todo-item d-flex align-items-center p-2 border-bottom"
_id $"todo-{todo.Id}"
] [
input [
_type "checkbox"
_class "form-check-input me-2"
_checked todo.Completed
_hxPut $"/todos/{todo.Id}/toggle"
_hxTarget $"#todo-{todo.Id}"
_hxSwap "outerHTML"
]
span [
_class (if todo.Completed then "completed" else "")
] [ str todo.Text ]
button [
_class "btn btn-sm btn-outline-danger ms-auto"
_hxDelete $"/todos/{todo.Id}"
_hxTarget $"#todo-{todo.Id}"
_hxSwap "outerHTML"
_hxConfirm "Are you sure you want to delete this todo?"
] [
i [ _class "bi bi-trash" ] []
]
]
// Todo list view
let todoListView () =
div [ _id "todo-list" ] [
yield! todos |> List.map todoItem
]
// Add todo form
let addTodoForm () =
form [
_hxPost "/todos"
_hxTarget "#todo-list"
_hxSwap "beforeend"
_class "mb-4"
] [
div [ _class "input-group" ] [
input [
_type "text"
_name "text"
_class "form-control"
_placeholder "Add a new todo..."
_required
]
button [
_type "submit"
_class "btn btn-primary"
] [
span [ _class "htmx-indicator spinner-border spinner-border-sm me-2" ] []
str "Add Todo"
]
]
]
// Main todo page
let todoPage () =
div [ _class "container mt-4" ] [
h1 [ _class "mb-4" ] [ str "📝 F# HTMX Todo App" ]
addTodoForm()
todoListView()
]
// Handlers using curried layout
let withTodoLayout = htmx todoLayout
let todoHandlers = [
GET >=> route "/" >=> withTodoLayout todoPage
POST >=> route "/todos" >=> fun next ctx -> task {
let! form = ctx.BindFormAsync<{| text: string |}>()
let newTodo = {
Id = (todos |> List.maxBy (_.Id)).Id + 1
Text = form.text
Completed = false
}
todos <- todos @ [newTodo]
return! htmlView (todoItem newTodo) next ctx
}
PUT >=> routef "/todos/%d/toggle" (fun id -> fun next ctx -> task {
todos <- todos |> List.map (fun t ->
if t.Id = id then { t with Completed = not t.Completed } else t)
let todo = todos |> List.find (fun t -> t.Id = id)
return! htmlView (todoItem todo) next ctx
})
DELETE >=> routef "/todos/%d" (fun id -> fun next ctx -> task {
todos <- todos |> List.filter (fun t -> t.Id <> id)
return! text "" next ctx
})
]
Advanced Form with Validation
type UserForm = {
Name: string
Email: string
Age: int option
}
let userFormView (form: UserForm option) (errors: string list) =
let form = defaultArg form { Name = ""; Email = ""; Age = None }
div [ _class "row justify-content-center" ] [
div [ _class "col-md-6" ] [
h2 [] [ str "User Registration" ]
// Error display
if not (List.isEmpty errors) then
div [ _class "alert alert-danger" ] [
ul [ _class "mb-0" ] [
yield! errors |> List.map (fun error ->
li [] [ str error ]
)
]
]
form [
_hxPost "/users/validate"
_hxTarget "#form-container"
_hxSwap "outerHTML"
_class "needs-validation"
_novalidate
] [
div [ _class "mb-3" ] [
label [ _for "name"; _class "form-label" ] [ str "Name" ]
input [
_type "text"
_id "name"
_name "name"
_class "form-control"
_value form.Name
_required
_hxPost "/users/validate-field"
_hxTrigger "blur"
_hxTarget "#name-feedback"
]
div [ _id "name-feedback"; _class "invalid-feedback" ] []
]
div [ _class "mb-3" ] [
label [ _for "email"; _class "form-label" ] [ str "Email" ]
input [
_type "email"
_id "email"
_name "email"
_class "form-control"
_value form.Email
_required
]
]
div [ _class "mb-3" ] [
label [ _for "age"; _class "form-label" ] [ str "Age (optional)" ]
input [
_type "number"
_id "age"
_name "age"
_class "form-control"
_min "1"
_max "120"
match form.Age with
| Some age -> _value (string age)
| None -> ()
]
]
button [
_type "submit"
_class "btn btn-primary"
] [
span [ _class "htmx-indicator spinner-border spinner-border-sm me-2" ] []
str "Register"
]
]
]
]
Real-time Updates with Server-Sent Events
let dashboardView () =
div [ _class "container mt-4" ] [
h1 [] [ str "📊 Real-time Dashboard" ]
// Auto-updating metrics
div [
_id "metrics"
_hxGet "/api/metrics"
_hxTrigger "every 2s"
_hxSwap "innerHTML"
] [
str "Loading metrics..."
]
// Live notifications
div [
_id "notifications"
_hxExt "sse"
_hxSse "connect:/events"
] [
div [
_hxSse "swap:notification"
_hxSwap "afterbegin"
] []
]
// Interactive chart that updates on click
div [ _class "mt-4" ] [
canvas [
_id "chart"
_hxGet "/api/chart-data"
_hxTrigger "click from:body"
_hxTarget "this"
_hxSwap "outerHTML"
] []
]
]
Type-Safe Trigger Compositions
open Giraffe.ViewEngine.Htmx.Attributes.HtmxTrigger
open Giraffe.ViewEngine.Htmx.Attributes.HtmxSwap
// Complex trigger with multiple modifiers
let searchInput =
input [
_type "text"
_name "query"
_class "form-control"
_placeholder "Search..."
_hxGet "/search"
_hxTarget "#results"
_hxTriggerTyped (
keyup
|> withKey "Enter"
|> delay 300
|> throttle 500
)
_hxSwapTyped (
innerHTML
|> withTransition
|> withSwapDelay 100
|> withScroll "top"
)
]
// Multiple triggers
let advancedButton =
button [
_class "btn btn-primary"
_hxPost "/api/action"
_hxTriggerTyped (
// Trigger on click OR Enter key
click |> once // Only fire once
)
_hxConfirm "Are you sure?"
] [ str "Advanced Action" ]
Advanced Features
Request Context Access
Access HTMX-specific request information:
let smartHandler: HttpHandler = fun next ctx ->
if ctx.IsHtmxRequest() then
// HTMX request - return fragment
let target = ctx.HtmxTarget() |> Option.defaultValue "unknown"
let trigger = ctx.HtmxTrigger() |> Option.defaultValue "unknown"
htmlView (
div [] [
str $"HTMX Request - Target: {target}, Trigger: {trigger}"
]
) next ctx
else
// Full page request
htmlView (appLayout [
h1 [] [ str "Full Page" ]
]) next ctx
Response Headers
Control client-side behavior with response headers:
let actionHandler: HttpHandler = fun next ctx -> task {
// Perform some action
// Set HTMX response headers
ctx.SetHtmxTrigger("refreshData") |> ignore
ctx.SetHtmxPushUrl("/new-url") |> ignore
return! htmlView (
div [ _class "alert alert-success" ] [
str "Action completed successfully!"
]
) next ctx
}
Custom Layout Configurations
// Development layout with debugging tools
let devLayout = htmxLayout {
title "Dev Mode - My App"
version V2_0_6
styles [
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
"/css/dev-tools.css"
]
scripts [
script [ _src "/js/dev-tools.js" ] []
]
head [
meta [ _name "environment"; _content "development" ]
]
bodyAttr [ _class "dev-mode"; attr "data-debug" "true" ]
}
// Production layout optimized for performance
let prodLayout = htmxLayout {
title "My Production App"
version V2_0_6
styles [
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
"/css/app.min.css"
]
head [
meta [ _name "description"; _content "Fast, modern web app built with F# and HTMX" ]
link [ _rel "icon"; _href "/favicon.ico" ]
]
}
API Reference
Core Attributes
Function | Description | Example |
---|---|---|
_hxGet |
HTTP GET request | _hxGet "/api/data" |
_hxPost |
HTTP POST request | _hxPost "/api/create" |
_hxPut |
HTTP PUT request | _hxPut "/api/update" |
_hxDelete |
HTTP DELETE request | _hxDelete "/api/delete" |
_hxTarget |
Target element | _hxTarget "#results" |
_hxSwap |
Swap strategy | _hxSwap "innerHTML" |
_hxTrigger |
Trigger event | _hxTrigger "click" |
Type-Safe Alternatives
Function | Description | Type |
---|---|---|
_hxTriggerTyped |
Type-safe triggers | HtmxTrigger |
_hxSwapTyped |
Type-safe swaps | HtmxSwap |
Context Extensions
Method | Description | Return Type |
---|---|---|
IsHtmxRequest() |
Check if HTMX request | bool |
HtmxTarget() |
Get target element | string option |
HtmxTrigger() |
Get trigger element | string option |
SetHtmxRedirect() |
Set redirect header | HttpContext |
SetHtmxPushUrl() |
Update browser URL | HttpContext |
Best Practices
1. Use Curried Layouts
let withMainLayout = htmx mainLayout
let withApiLayout = htmx apiLayout
// Clean, reusable handlers
let homeHandler = withMainLayout (fun () -> homeView())
let profileHandler = withMainLayout (fun () -> profileView())
2. Organize by Feature
module Users =
let private withLayout = htmx userLayout
let listHandler = withLayout (fun () -> userListView())
let detailHandler id = withLayout (fun () -> userDetailView id)
let createHandler = withLayout (fun () -> userCreateView())
3. Type-Safe Compositions
// Prefer type-safe builders for complex scenarios
let complexTrigger =
HtmxTrigger.keyup
|> HtmxTrigger.withKey "Enter"
|> HtmxTrigger.delay 300
|> HtmxTrigger.once
button [ _hxTriggerTyped complexTrigger ] [ str "Submit" ]
4. Progressive Enhancement
// Always provide fallbacks
form [
_action "/users" // Works without JavaScript
_method "POST"
_hxPost "/users" // Enhanced with HTMX
_hxTarget "#result"
] [
(* form content *)
]
Contributing
Contributions are welcome! Please see our Contributing Guide for details.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Related Projects
- Giraffe - F# web framework
- HTMX - High power tools for HTML
- Giraffe.ViewEngine - F# HTML DSL
Built with ❤️ by the F# community
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net6.0
- FSharp.Core (>= 9.0.300)
- Giraffe (>= 7.0.2)
- Giraffe.ViewEngine (>= 1.4.0)
-
net8.0
- FSharp.Core (>= 9.0.300)
- Giraffe (>= 7.0.2)
- Giraffe.ViewEngine (>= 1.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last Updated |
---|---|---|
1.0.0 | 277 | 7/25/2025 |