Compare commits

...

3 Commits

Author SHA1 Message Date
danieljsummers cfd8d7eb68 v2,0.10 (#21)
Update script to 2.0.10, convert sln to slnx

Reviewed-on: #21
2026-07-03 23:44:51 +00:00
danieljsummers 8f93aa4d35 Add htmx 4 notes to README 2026-06-30 14:02:34 +00:00
danieljsummers 121eb95d87 v2.0.8 (#16)
- Embed htmx library to `Giraffe.Htmx.Common`, add links to load package-provided script
- Update CDN links for v2.0.8
- Add .NET 10 support

Reviewed-on: #16
2025-12-28 16:53:44 +00:00
15 changed files with 254 additions and 222 deletions
+7 -1
View File
@@ -6,6 +6,8 @@
htmx uses attributes and HTTP headers to attain its interactivity; the libraries here contain extensions to both Giraffe and Giraffe View Engine to enable strongly-typed development of htmx applications.
> **NOTE** htmx v4 is curently in beta; this library is supporting it on the [`htmx-v4` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v4/). Beta versions of the v4 library include warnings about obsolete constructs. The htmx project intends to continue supporting the 2.x line (just as they still do the 1.x line, which we track on the [`htmx-v1` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v1/)); the upgrade is available, not forced. See the [htmx v4 migration docs](https://four.htmx.org/docs#migration) for more information.
## Installation
`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods.
@@ -14,6 +16,8 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
|---|---|
|[![Nuget](https://img.shields.io/nuget/v/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/)|[![Nuget](https://img.shields.io/nuget/v/Giraffe.ViewEngine.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)|
Both of these packages will also install `Giraffe.Htmx.Common`, which has some common definitions and provides a local-to-your-project version of the htmx JavaScript _(as of v2.0.8)_.
## Server Side (`Giraffe.Htmx`)
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request.
@@ -42,6 +46,8 @@ let theHandler : HttpHandler =
Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well.
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library. This can be used in code, Razor templates, etc. (If you're using Giraffe.ViewEngine, see below.)
## View Engine (`Giraffe.ViewEngine.Htmx`)
As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible.
@@ -66,7 +72,7 @@ let shiftClick =
]
```
If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees.
If you want to use the package-provided htmx library, `Htmx.Script.local` will create the `script` tag for you. To load htmx from jsDelivr, `Htmx.Script.cdnMinified` or `Htmx.Script.cdnUnminified` can be used to load the script in your HTML trees. In this case, if you are using a Content Security Policy (CSP) header, `cdn.jsdelivr.net` will need to be added to the `script-src` list.
## Feedback / Help
+13 -7
View File
@@ -2,6 +2,12 @@
[<AutoOpen>]
module Giraffe.Htmx.Common
/// <summary>The version of htmx embedded in the package</summary>
let HtmxVersion = "2.0.10"
/// <summary>The path for the provided htmx script</summary>
let internal htmxLocalScript = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
/// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary>
/// <param name="pairs">The key/value pairs to be serialized to JSON</param>
/// <returns>A string with the key/value pairs serialized to JSON</returns>
@@ -23,31 +29,31 @@ let internal toLowerBool (boolValue: bool) =
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxSwap =
/// <summary>The default, replace the inner HTML of the target element</summary>
[<Literal>]
let InnerHtml = "innerHTML"
/// <summary>Replace the entire target element with the response</summary>
[<Literal>]
let OuterHtml = "outerHTML"
/// <summary>Insert the response before the target element</summary>
[<Literal>]
let BeforeBegin = "beforebegin"
/// <summary>Insert the response before the first child of the target element</summary>
[<Literal>]
let AfterBegin = "afterbegin"
/// <summary>Insert the response after the last child of the target element</summary>
[<Literal>]
let BeforeEnd = "beforeend"
/// <summary>Insert the response after the target element</summary>
[<Literal>]
let AfterEnd = "afterend"
/// <summary>Does not append content from response (out of band items will still be processed).</summary>
[<Literal>]
let None = "none"
+1 -1
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
+2 -2
View File
@@ -1,5 +1,5 @@
## Giraffe.Htmx.Common
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one.
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx as a static web asset, allowing it to be loaded from your local (or published) project.
**htmx version: 2.0.6**
**htmx version: 2.0.10**
File diff suppressed because one or more lines are too long
+5 -8
View File
@@ -1,15 +1,12 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<VersionPrefix>2.0.6</VersionPrefix>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<VersionPrefix>2.0.10</VersionPrefix>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>- All packages now have full XML documentation
- Adds HxSync module and attribute helper to view engine
- Updates script tags to pull htmx 2.0.6 (no header or attribute changes)
- Drops .NET 6 support
NOTE: The CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed domains accordingly
<PackageReleaseNotes>- Updates provided script and CDN links to serve htmx 2.0.10 (no header or attribute changes)
NOTE: As of 2.0.6, the CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed script-src domains accordingly
</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
-40
View File
@@ -1,40 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx", "Htmx\Giraffe.Htmx.fsproj", "{8AB3085C-5236-485A-8565-A09106E72E1E}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.ViewEngine.Htmx", "ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj", "{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx.Common", "Common\Giraffe.Htmx.Common.fsproj", "{75D66845-F93A-4463-AD29-A8B16E4D4BA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{39823773-4311-4E79-9CA0-F9DDC40CAF6A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.Build.0 = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.Build.0 = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.Build.0 = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+6
View File
@@ -0,0 +1,6 @@
<Solution>
<Project Path="Common/Giraffe.Htmx.Common.fsproj" />
<Project Path="Htmx/Giraffe.Htmx.fsproj" />
<Project Path="Tests/Tests.fsproj" />
<Project Path="ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj" />
</Solution>
+12
View File
@@ -170,3 +170,15 @@ module Handlers =
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap"
/// <summary>Load the package-provided version of the htmx script</summary>
[<RequireQualifiedAccess>]
module HtmxScript =
open Giraffe.Htmx.Common
open Microsoft.AspNetCore.Html
/// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary>
let local = HtmlString $"""<script src="{htmxLocalScript}"></script>"""
+3 -1
View File
@@ -2,7 +2,7 @@
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 2.0.6**
**htmx version: 2.0.10**
_Upgrading from v1.x: the [migration guide](https://htmx.org/migration-guide-htmx-1/) does not currently specify any request or response header changes. This means that there are no required code changes in moving from v1.* to v2.*._
@@ -34,6 +34,8 @@ let myHandler : HttpHander =
The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information.
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`.
### Learn
The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events.
+7 -1
View File
@@ -3,6 +3,12 @@ module Common
open Expecto
open Giraffe.Htmx
/// Test to ensure the version was updated
let version =
test "HtmxVersion is correct" {
Expect.equal HtmxVersion "2.0.10" "htmx version incorrect"
}
/// Tests for the HxSwap module
let swap =
testList "HxSwap" [
@@ -30,4 +36,4 @@ let swap =
]
/// All tests for this module
let allTests = testList "Htmx.Common" [ swap ]
let allTests = testList "Htmx.Common" [ version; swap ]
+17 -5
View File
@@ -3,6 +3,7 @@ module Htmx
open System
open Expecto
open Giraffe.Htmx
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Http
open NSubstitute
@@ -166,7 +167,7 @@ let reqExtensions =
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request"
}
}
test "succeeds when request is from htmx" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
@@ -203,7 +204,7 @@ open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate
let next (ctx: HttpContext) = Task.FromResult(Some ctx)
/// Tests for the HttpHandler functions provided in the Handlers module
let handlers =
testList "HandlerTests" [
@@ -280,7 +281,7 @@ let handlers =
let! _ = withHxReselect "#test" next ctx
Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present"
Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect"
}
}
testTask "withHxReswap succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
@@ -288,7 +289,7 @@ let handlers =
let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present"
Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect"
}
}
testTask "withHxRetarget succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
@@ -354,5 +355,16 @@ let handlers =
}
]
/// Tests for the HtmxScript module
let script =
testList "HtmxScript" [
test "local generates correct link" {
Expect.equal
(string HtmxScript.local)
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
"htmx script link is incorrect"
}
]
/// All tests for this module
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers ]
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script ]
+22 -13
View File
@@ -610,7 +610,7 @@ let hxTrigger =
testList "FromDocument" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.FromDocument "") "from:document" "FromDocument modifier incorrect"
}
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.FromDocument "click") "click from:document" "FromDocument modifier incorrect"
}
@@ -715,7 +715,7 @@ let hxVals =
]
]
/// Pipe-able assertion for a rendered node
/// Pipe-able assertion for a rendered node
let shouldRender expected node =
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
@@ -842,22 +842,31 @@ let attributes =
}
]
open Giraffe.Htmx.Common
/// Tests for the Script module
let script =
testList "Script" [
test "minified succeeds" {
let html = RenderView.AsString.htmlNode Script.minified
test "local succeeds" {
let html = RenderView.AsString.htmlNode Script.local
Expect.equal
html
"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js" integrity="sha384-Akqfrbj/HpNVo8k11SXBb6TlBWmXXlYQrCSqEWmyKJe+hDm3Z/B2WVG4smwBkRVm" crossorigin="anonymous"></script>"""
"Minified script tag is incorrect"
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
"Local script tag is incorrect"
}
test "unminified succeeds" {
let html = RenderView.AsString.htmlNode Script.unminified
test "cdnMinified succeeds" {
let html = RenderView.AsString.htmlNode Script.cdnMinified
Expect.equal
html
"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.js" integrity="sha384-ksKjJrwjL5VxqAkAZAVOPXvMkwAykMaNYegdixAESVr+KqLkKE8XBDoZuwyWVUDv" crossorigin="anonymous"></script>"""
"Unminified script tag is incorrect"
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script>"""
"CDN minified script tag is incorrect"
}
test "cdnUnminified succeeds" {
let html = RenderView.AsString.htmlNode Script.cdnUnminified
Expect.equal
html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-Q+Dky3iHVJOr6wUjQ4ulh6uQ76an/t+ak1+PjMVaxRjbZamFLAG+u9InkfjbsEQf" crossorigin="anonymous"></script>"""
"CDN unminified script tag is incorrect"
}
]
@@ -866,11 +875,11 @@ open System.Text
/// Tests for the RenderFragment module
let renderFragment =
testList "RenderFragment" [
/// Validate that the two object references are the same object
let isSame obj1 obj2 message =
Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message
testList "findIdNode" [
test "fails with a Text node" {
Expect.isNone (RenderFragment.findIdNode "blue" (Text "")) "There should not have been a node found"
@@ -936,7 +945,7 @@ let renderFragment =
]
]
testList "AsBytes" [
/// Alias for UTF-8 encoding
let utf8 = Encoding.UTF8
+156 -141
View File
@@ -4,11 +4,11 @@ module Giraffe.ViewEngine.Htmx
/// <summary>Valid values for the <c>hx-encoding</c> attribute</summary>
[<RequireQualifiedAccess>]
module HxEncoding =
/// <summary>A standard HTTP form</summary>
[<Literal>]
let Form = "application/x-www-form-urlencoded"
/// <summary>A multipart form (used for file uploads)</summary>
[<Literal>]
let MultipartForm = "multipart/form-data"
@@ -17,137 +17,137 @@ module HxEncoding =
/// <summary>The events recognized by htmx</summary>
[<Struct>]
type HxEvent =
/// <summary>Send this event to an element to abort a request</summary>
| Abort
/// <summary>Triggered after an AJAX request has completed processing a successful response</summary>
| AfterOnLoad
/// <summary>Triggered after htmx has initialized a node</summary>
| AfterProcessNode
/// <summary>Triggered after an AJAX request has completed</summary>
| AfterRequest
/// <summary>Triggered after the DOM has settled</summary>
| AfterSettle
/// <summary>Triggered after new content has been swapped in</summary>
| AfterSwap
/// <summary>Triggered before htmx disables an element or removes it from the DOM</summary>
| BeforeCleanupElement
/// <summary>Triggered before any response processing occurs</summary>
| BeforeOnLoad
/// <summary>Triggered before htmx initializes a node</summary>
| BeforeProcessNode
/// <summary>Triggered before an AJAX request is made</summary>
| BeforeRequest
/// <summary>Triggered before a swap is done, allows you to configure the swap</summary>
| BeforeSwap
/// <summary>Triggered just before an ajax request is sent</summary>
| BeforeSend
/// <summary>Triggered before the request, allows you to customize parameters, headers</summary>
| ConfigRequest
/// <summary>
/// Triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request
/// </summary>
| Confirm
/// <summary>Triggered on an error during cache writing</summary>
| HistoryCacheError
/// <summary>Triggered on a cache miss in the history subsystem</summary>
| HistoryCacheMiss
/// <summary>Triggered on a unsuccessful remote retrieval</summary>
| HistoryCacheMissError
/// <summary>Triggered on a successful remote retrieval</summary>
| HistoryCacheMissLoad
/// <summary>Triggered when htmx handles a history restoration action</summary>
| HistoryRestore
/// <summary>Triggered before content is saved to the history cache</summary>
| BeforeHistorySave
/// <summary>Triggered when new content is added to the DOM</summary>
| Load
/// <summary>
/// Triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined
/// </summary>
| NoSseSourceError
/// <summary>Triggered when an exception occurs during the onLoad handling in htmx</summary>
| OnLoadError
/// <summary>Triggered after an out of band element as been swapped in</summary>
| OobAfterSwap
/// <summary>Triggered before an out of band element swap is done, allows you to configure the swap</summary>
| OobBeforeSwap
/// <summary>Triggered when an out of band element does not have a matching ID in the current DOM</summary>
| OobErrorNoTarget
/// <summary>Triggered after a prompt is shown</summary>
| Prompt
/// <summary>Triggered after an url is pushed into history</summary>
| PushedIntoHistory
/// <summary>Triggered when an HTTP response error (non-200 or 300 response code) occurs</summary>
| ResponseError
/// <summary>Triggered when a network error prevents an HTTP request from happening</summary>
| SendError
/// <summary>Triggered when an error occurs with a SSE source</summary>
| SseError
/// <summary>Triggered when a SSE source is opened</summary>
| SseOpen
/// <summary>Triggered when an error occurs during the swap phase</summary>
| SwapError
/// <summary>Triggered when an invalid target is specified</summary>
| TargetError
/// <summary>Triggered when a request timeout occurs</summary>
| Timeout
/// <summary>Triggered before an element is validated</summary>
| ValidationValidate
/// <summary>Triggered when an element fails validation</summary>
| ValidationFailed
/// <summary>Triggered when a request is halted due to validation errors</summary>
| ValidationHalted
/// <summary>Triggered when an ajax request aborts</summary>
| XhrAbort
/// <summary>Triggered when an ajax request ends</summary>
| XhrLoadEnd
/// <summary>Triggered when an ajax request starts</summary>
| XhrLoadStart
/// <summary>Triggered periodically during an ajax request that supports progress events</summary>
| XhrProgress
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
static member private Values = Map [
Abort, ("abort", "abort")
@@ -193,10 +193,10 @@ type HxEvent =
XhrLoadStart, ("xhr:loadstart", "xhr:loadstart")
XhrProgress, ("xhr:progress", "xhr:progress")
]
/// <summary>The htmx event name</summary>
/// <summary>The htmx event name</summary>
override this.ToString() = fst HxEvent.Values[this]
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary>
member this.ToHxOnString() = snd HxEvent.Values[this]
@@ -204,7 +204,7 @@ type HxEvent =
/// <summary>Helper to create the <c>hx-headers</c> attribute</summary>
[<RequireQualifiedAccess>]
module HxHeaders =
/// <summary>Create headers from a list of key/value pairs</summary>
let From = Giraffe.Htmx.Common.toJson
@@ -213,21 +213,21 @@ module HxHeaders =
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxParams =
/// <summary>Include all parameters</summary>
[<Literal>]
let All = "*"
/// <summary>Include no parameters</summary>
[<Literal>]
let None = "none"
/// <summary>Include the specified parameters</summary>
/// <param name="fields">One or more fields to include in the request</param>
/// <returns>The list of fields for the <c>hx-params</c> attribute value</returns>
let With fields =
match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
/// <summary>Exclude the specified parameters</summary>
/// <param name="fields">One or more fields to exclude from the request</param>
/// <returns>The list of fields for the <c>hx-params</c> attribute value prefixed with "not"</returns>
@@ -239,9 +239,9 @@ module HxParams =
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxRequest =
open Giraffe.Htmx.Common
/// <summary>Configure the request with various options</summary>
/// <param name="opts">The options to configure</param>
/// <returns>A string with the configured options</returns>
@@ -249,19 +249,19 @@ module HxRequest =
opts
|> String.concat ", "
|> sprintf "{ %s }"
/// <summary>Set a timeout (in milliseconds)</summary>
/// <param name="ms">The milliseconds for the request timeout</param>
/// <returns>A string with the configured request timeout</returns>
let Timeout (ms: int) =
$"\"timeout\": {ms}"
/// <summary>Include or exclude credentials from the request</summary>
/// <param name="send"><c>true</c> if credentials should be sent, <c>false</c> if not</param>
/// <returns>A string with the configured credential options</returns>
let Credentials send =
(toLowerBool >> sprintf "\"credentials\": %s") send
/// <summary>Exclude or include headers from the request</summary>
/// <param name="exclude">
/// <c>true</c> if no headers should be sent; <c>false</c> if headers should be sent
@@ -275,35 +275,35 @@ module HxRequest =
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxSync =
/// <summary>Drop (ignore) this request if a request is already in flight</summary>
/// <remarks>This is the default for <c>hx-sync</c></remarks>
[<Literal>]
let Drop = "drop"
/// <summary>
/// Drop (ignore) this request if a request is already in flight, and if another request occurs while this one is in
/// flight, abort this request
/// </summary>
[<Literal>]
let Abort = "abort"
/// <summary>Abort any current in-flight request and replace it with this one</summary>
[<Literal>]
let Replace = "replace"
/// <summary>Place this request in an element-associated queue</summary>
[<Literal>]
let Queue = "queue"
/// <summary>Queue only the first request received while another request is in flight</summary>
[<Literal>]
let QueueFirst = "queue first"
/// <summary>Queue only the last request received while another request is in flight</summary>
[<Literal>]
let QueueLast = "queue last"
/// <summary>Queue all requests received while another request is in flight</summary>
[<Literal>]
let QueueAll = "queue all"
@@ -313,7 +313,7 @@ module HxSync =
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxTrigger =
/// Append a filter to a trigger
let private appendFilter filter (trigger : string) =
match trigger.Contains "[" with
@@ -321,125 +321,125 @@ module HxTrigger =
let parts = trigger.Split ('[', ']')
$"{parts[0]}[{parts[1]}&&{filter}]"
| false -> $"{trigger}[{filter}]"
/// <summary>Trigger the event on a click</summary>
[<Literal>]
let Click = "click"
/// <summary>Trigger the event on page load</summary>
[<Literal>]
let Load = "load"
/// <summary>Trigger the event when the item is visible</summary>
[<Literal>]
let Revealed = "revealed"
/// <summary>Trigger this event every [timing declaration]</summary>
/// <param name="duration">The duration on which this trigger should fire (e.g., "1s", "500ms")</param>
/// <returns>A trigger timing specification</returns>
let Every duration =
$"every %s{duration}"
/// <summary>Helpers for defining filters</summary>
module Filter =
/// <summary>Only trigger the event if the <c>ALT</c> key is pressed</summary>
let Alt = appendFilter "altKey"
/// <summary>Only trigger the event if the <c>CTRL</c> key is pressed</summary>
let Ctrl = appendFilter "ctrlKey"
/// <summary>Only trigger the event if the <c>SHIFT</c> key is pressed</summary>
let Shift = appendFilter "shiftKey"
/// <summary>Only trigger the event if <c>CTRL</c> and <c>ALT</c> are pressed</summary>
let CtrlAlt = Ctrl >> Alt
/// <summary>Only trigger the event if <c>CTRL</c> and <c>SHIFT</c> are pressed</summary>
let CtrlShift = Ctrl >> Shift
/// <summary>Only trigger the event if <c>CTRL</c>, <c>ALT</c>, and <c>SHIFT</c> are pressed</summary>
let CtrlAltShift = CtrlAlt >> Shift
/// <summary>Only trigger the event if <c>ALT</c> and <c>SHIFT</c> are pressed</summary>
let AltShift = Alt >> Shift
/// Append a modifier to the current trigger
let private appendModifier modifier current =
if current = "" then modifier else $"{current} {modifier}"
/// <summary>Only trigger once</summary>
/// <param name="action">The action which should only be fired once</param>
/// <returns>A trigger spec to fire the given action once</returns>
let Once action =
appendModifier "once" action
/// <summary>Trigger when changed</summary>
/// <param name="elt">The element from which the <c>onchange</c> event will be emitted</param>
/// <returns>A trigger spec to fire when the given element changes</returns>
let Changed elt =
appendModifier "changed" elt
/// <summary>Delay execution; resets every time the event is seen</summary>
/// <param name="duration">The duration for the delay (e.g., "1s", "500ms")</param>
/// <param name="action">The action which should be fired after the given delay</param>
/// <returns>A trigger spec to fire the given action after the specified delay</returns>
let Delay duration action =
appendModifier $"delay:%s{duration}" action
/// <summary>Throttle execution; ignore other events, fire when duration passes</summary>
/// <param name="duration">The duration for the throttling (e.g., "1s", "500ms")</param>
/// <param name="action">The action which should be fired after the given duration</param>
/// <returns>A trigger spec to fire the given action after the specified duration</returns>
let Throttle duration action =
appendModifier $"throttle:%s{duration}" action
/// <summary>Trigger this event from a CSS selector</summary>
/// <param name="selector">A CSS selector to identify elements which may fire this trigger</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the given element(s)</returns>
let From selector action =
appendModifier $"from:%s{selector}" action
/// <summary>Trigger this event from the <c>document</c> object</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the <c>document</c> element</returns>
let FromDocument action =
From "document" action
/// <summary>Trigger this event from the <c>window</c> object</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the <c>window</c> object</returns>
let FromWindow action =
From "window" action
/// <summary>Trigger this event from the closest parent CSS selector</summary>
/// <param name="selector">The CSS selector from which the action should be fired</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest element</returns>
let FromClosest selector action =
From $"closest %s{selector}" action
/// <summary>Trigger this event from the closest child CSS selector</summary>
/// <param name="selector">The child CSS selector from which the action should be fired</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest child element</returns>
let FromFind selector action =
From $"find %s{selector}" action
/// <summary>Target the given CSS selector with the results of this event</summary>
/// <param name="selector">The CSS selector to which the result of the action will be applied</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to target the given selector</returns>
let Target selector action =
appendModifier $"target:%s{selector}" action
/// <summary>Prevent any further events from occurring after this one fires</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire the given action and prevent further events</returns>
let Consume action =
appendModifier "consume" action
/// <summary>
/// Configure queueing when events fire when others are in flight; if unspecified, the default is <c>last</c>
/// </summary>
@@ -452,25 +452,25 @@ module HxTrigger =
let Queue how action =
let qSpec = if how = "" then "" else $":{how}"
appendModifier $"queue{qSpec}" action
/// <summary>Queue the first event, discard all others (i.e., a FIFO queue of 1)</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueFirst action =
Queue "first" action
/// <summary>Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueLast action =
Queue "last" action
/// <summary>Queue all events; discard none</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueAll action =
Queue "all" action
/// <summary>Queue no events; discard all</summary>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
@@ -482,7 +482,7 @@ module HxTrigger =
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxVals =
/// <summary>Create values from a list of key/value pairs</summary>
let From = Giraffe.Htmx.Common.toJson
@@ -492,30 +492,30 @@ open Giraffe.Htmx
/// <summary>Attributes and flags for htmx</summary>
[<AutoOpen>]
module HtmxAttrs =
/// <summary>Progressively enhances anchors and forms to use AJAX requests</summary>
/// <remarks>Use <c>_hxNoBoost</c> to set to false</remarks>
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
let _hxBoost = attr "hx-boost" "true"
/// <summary>Shows a <c>confirm()</c> dialog before issuing a request</summary>
/// <param name="prompt">The prompt to present to the user when seeking their confirmation</param>
/// <returns>A configured <c>hx-confirm</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-confirm/">Documentation</seealso>
let _hxConfirm prompt =
attr "hx-confirm" prompt
/// <summary>Issues a <c>DELETE</c> to the specified URL</summary>
/// <param name="url">The URL to which the <c>DELETE</c> request should be sent</param>
/// <returns>A configured <c>hx-delete</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-delete/">Documentation</seealso>
let _hxDelete url =
attr "hx-delete" url
/// <summary>Disables htmx processing for the given node and any children nodes</summary>
/// <seealso href="https://htmx.org/attributes/hx-disable/">Documentation</seealso>
let _hxDisable = flag "hx-disable"
/// <summary>Specifies elements that should be disabled when an htmx request is in flight</summary>
/// <param name="elt">The element to disable when an htmx request is in flight</param>
/// <returns>A configured <c>hx-disabled-elt</c> attribute</returns>
@@ -529,7 +529,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-disinherit/">Documentation</seealso>
let _hxDisinherit hxAttrs =
attr "hx-disinherit" hxAttrs
/// <summary>Changes the request encoding type</summary>
/// <param name="enc">The encoding type (use <c>HxEncoding</c> constants)</param>
/// <returns>A configured <c>hx-encoding</c> attribute</returns>
@@ -537,28 +537,28 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-encoding/">Documentation</seealso>
let _hxEncoding enc =
attr "hx-encoding" enc
/// <summary>Extensions to use for this element</summary>
/// <param name="exts">A list of extensions to apply to this element</param>
/// <returns>A configured <c>hx-ext</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-ext/">Documentation</seealso>
let _hxExt exts =
attr "hx-ext" exts
/// <summary>Issues a <c>GET</c> to the specified URL</summary>
/// <param name="url">The URL to which the <c>GET</c> request should be sent</param>
/// <returns>A configured <c>hx-get</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-get/">Documentation</seealso>
let _hxGet url =
attr "hx-get" url
/// <summary>Adds to the headers that will be submitted with the request</summary>
/// <param name="hdrs">The headers to include with the request</param>
/// <returns>A configured <c>hx-headers</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-headers/">Documentation</seealso>
let _hxHeaders hdrs =
attr "hx-headers" hdrs
/// <summary>
/// Set to "false" to prevent pages with sensitive information from being stored in the history cache
/// </summary>
@@ -567,31 +567,31 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-history/">Documentation</seealso>
let _hxHistory shouldStore =
attr "hx-history" (toLowerBool shouldStore)
/// <summary>The element to snapshot and restore during history navigation</summary>
/// <seealso href="https://htmx.org/attributes/hx-history-elt/">Documentation</seealso>
let _hxHistoryElt =
flag "hx-history-elt"
/// <summary>Includes additional data in AJAX requests</summary>
/// <param name="spec">The specification of what should be included in the request</param>
/// <returns>A configured <c>hx-include</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-include/">Documentation</seealso>
let _hxInclude spec =
attr "hx-include" spec
/// <summary>The element to put the <c>htmx-request</c> class on during the AJAX request</summary>
/// <param name="selector">The selector for the indicator element</param>
/// <returns>A configured <c>hx-indicator</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-indicator/">Documentation</seealso>
let _hxIndicator selector =
attr "hx-indicator" selector
/// <summary>Overrides a previous <c>hx-boost</c> (hx-boost="false")</summary>
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
let _hxNoBoost =
attr "hx-boost" "false"
/// <summary>Attach an event handler for DOM events</summary>
/// <param name="evtName">The name of the event</param>
/// <param name="handler">The script to be executed when the event occurs</param>
@@ -616,26 +616,26 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
let _hxParams toInclude =
attr "hx-params" toInclude
/// <summary>Issues a <c>PATCH</c> to the specified URL</summary>
/// <param name="url">The URL to which the request should be directed</param>
/// <returns>A configured <c>hx-patch</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-patch/">Documentation</seealso>
let _hxPatch url =
attr "hx-patch" url
/// <summary>Issues a <c>POST</c> to the specified URL</summary>
/// <param name="url">The URL to which the request should be directed</param>
/// <returns>A configured <c>hx-post</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-post/">Documentation</seealso>
let _hxPost url =
attr "hx-post" url
/// <summary>Preserves an element between requests</summary>
/// <seealso href="https://htmx.org/attributes/hx-preserve/">Documentation</seealso>
let _hxPreserve =
flag "hx-preserve"
/// <summary>Shows a <c>prompt()</c> dialog before submitting a request</summary>
/// <param name="text">The text for the prompt</param>
/// <returns>A configured <c>hx-prompt</c> attribute</returns>
@@ -643,7 +643,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso>
let _hxPrompt text =
attr "hx-prompt" text
/// <summary>Pushes the URL into the location bar, creating a new history entry</summary>
/// <param name="spec">
/// <ul>
@@ -656,14 +656,14 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-push-url/">Documentation</seealso>
let _hxPushUrl spec =
attr "hx-push-url" spec
/// <summary>Issues a <c>PUT</c> to the specified URL</summary>
/// <param name="url">The URL to which the request should be directed</param>
/// <returns>A configured <c>hx-put</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-put/">Documentation</seealso>
let _hxPut url =
attr "hx-put" url
/// <summary>Replaces the current URL in the browser's history stack</summary>
/// <param name="spec">
/// <ul>
@@ -676,7 +676,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-replace-url/">Documentation</seealso>
let _hxReplaceUrl spec =
attr "hx-replace-url" spec
/// <summary>Configures various aspects of the request</summary>
/// <param name="spec">The configuration spec (use <c>HxRequest.Configure</c> to create value)</param>
/// <returns>A configured <c>hx-request</c> attribute</returns>
@@ -684,21 +684,21 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
let _hxRequest spec =
attr "hx-request" spec
/// <summary>Selects a subset of the server response to process</summary>
/// <param name="selector">A CSS selector for the content to be selected</param>
/// <returns>A configured <c>hx-select</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-select/">Documentation</seealso>
let _hxSelect selector =
attr "hx-select" selector
/// <summary>Selects a subset of an out-of-band server response</summary>
/// <param name="selectors">One or more comma-delimited CSS selectors for the content to be selected</param>
/// <returns>A configured <c>hx-select-oob</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-select-oob/">Documentation</seealso>
let _hxSelectOob selectors =
attr "hx-select-oob" selectors
/// <summary>
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>)
/// </summary>
@@ -707,7 +707,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
let _hxSwap swap =
attr "hx-swap" swap
/// <summary>
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>), enabling
/// CSS transitions
@@ -717,7 +717,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
let _hxSwapWithTransition swap =
_hxSwap $"%s{swap} transition:true"
/// <summary>
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
/// </summary>
@@ -732,7 +732,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-swap-oob/">Documentation</seealso>
let _hxSwapOob swap =
attr "hx-swap-oob" swap
/// <summary>Synchronize events based on another element</summary>
/// <param name="selector">A CSS selector for the element with which this one should sync</param>
/// <param name="action">The request synchronization action to perform (use <c>HxSync</c> values)</param>
@@ -741,14 +741,14 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
let _hxSync selector action =
attr "hx-sync" $"%s{selector}:%s{action}"
/// <summary>Specifies the target element to be swapped</summary>
/// <param name="selector">A CSS selector or relative reference (or both) to identify the target</param>
/// <returns>A configured <c>hx-target</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-target/">Documentation</seealso>
let _hxTarget selector =
attr "hx-target" selector
/// <summary>Specifies the event that triggers the request</summary>
/// <param name="spec">The trigger specification (use <c>HxTrigger</c> to create)</param>
/// <returns>A configured <c>hx-trigger</c> attribute</returns>
@@ -756,12 +756,12 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
let _hxTrigger spec =
attr "hx-trigger" spec
/// <summary>Validate an input element (uses HTML5 validation API)</summary>
/// <seealso href="https://htmx.org/attributes/hx-validate/">Documentation</seealso>
let _hxValidate =
flag "hx-validate"
/// <summary>Adds to the parameters that will be submitted with the request</summary>
/// <param name="values">The values for the parameters (use <c>HxVals.From</c> to create)</param>
/// <returns>A configured <c>hx-vals</c> attribute</returns>
@@ -769,7 +769,7 @@ module HtmxAttrs =
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
let _hxVals values =
attr "hx-vals" values
/// <summary>The URL of the SSE server</summary>
/// <param name="url">The URL from which events will be received</param>
/// <returns>A configured <c>sse-connect</c> attribute</returns>
@@ -787,24 +787,39 @@ module HtmxAttrs =
/// <summary>Script tags to pull htmx into a web page</summary>
module Script =
/// <summary>Script tag to load the minified version from unpkg.com</summary>
let minified =
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js"
_integrity "sha384-Akqfrbj/HpNVo8k11SXBb6TlBWmXXlYQrCSqEWmyKJe+hDm3Z/B2WVG4smwBkRVm"
open System
/// <summary>Script tag to load the package-provided version of htmx</summary>
let local = script [ _src htmxLocalScript ] []
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnMinified =
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js"
_integrity "sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V"
_crossorigin "anonymous" ] []
/// <summary>Script tag to load the unminified version from unpkg.com</summary>
let unminified =
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.js"
_integrity "sha384-ksKjJrwjL5VxqAkAZAVOPXvMkwAykMaNYegdixAESVr+KqLkKE8XBDoZuwyWVUDv"
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary>
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnUnminified =
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js"
_integrity "sha384-Q+Dky3iHVJOr6wUjQ4ulh6uQ76an/t+ak1+PjMVaxRjbZamFLAG+u9InkfjbsEQf"
_crossorigin "anonymous" ] []
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
[<Obsolete "Use cdnMinified instead; this will be removed in v4">]
let minified = cdnMinified
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary>
[<Obsolete "Use cdnUnminified instead; this will be removed in v4">]
let unminified = cdnUnminified
/// <summary>Functions to extract and render an HTML fragment from a document</summary>
[<RequireQualifiedAccess>]
module RenderFragment =
/// Does this element have an ID matching the requested ID name?
let private isIdElement nodeId (elt: XmlElement) =
snd elt
@@ -816,7 +831,7 @@ module RenderFragment =
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId: string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// <summary>Find the node with the named ID</summary>
/// <param name="nodeId">The <c>id</c> attribute to find</param>
/// <param name="node">The node tree to search</param>
@@ -827,7 +842,7 @@ module RenderFragment =
| VoidElement elt -> if isIdElement nodeId elt then Some node else None
| ParentNode (elt, children) ->
if isIdElement nodeId elt then Some node else children |> List.tryPick (findIdNode nodeId)
/// <summary>Functions to render a fragment as a string</summary>
[<RequireQualifiedAccess>]
module AsString =
+2 -2
View File
@@ -2,7 +2,7 @@
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
**htmx version: 2.0.6**
**htmx version: 2.0.10**
_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_
@@ -29,7 +29,7 @@ Support modules include:
- `HxTrigger`
- `HxVals`
There are two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging).
`Htmx.Script.local` creates an `XmlNode` to load the package-provided htmx library. There are also two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.cdnMinified` loads the minified version, and `Htmx.Script.cdnUnminified` loads the unminified version (useful for debugging). When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`.
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).