9 Commits

Author SHA1 Message Date
061f6e5a4e Add link to frag render post 2022-11-24 11:34:07 -05:00
bb2df73175 Add _hxValidate, fragment rendering
Also bump version to 1.8.4
2022-11-24 10:50:08 -05:00
e0c567098d Add files to package common project 2022-07-14 11:32:42 -04:00
4be5bad8ef Update package READMEs 2022-07-14 09:24:49 -04:00
a169000ce2 Update for version 1.8.0 (#6)
- Bump library version
- Bump htmx script version
- Add hx-replace-url / HX-Replace-Url
- Add hx-select-oob
- Change hx-push-url to accept a string
- Add HX-Push-Url, obsolete HX-Push
- Implement HX-Reswap header
- Rework project structure to add common project
2022-07-14 09:13:52 -04:00
c587a28770 Update for htmx 1.7.0 (#4)
Fixes #3
2022-02-23 21:54:51 -05:00
b5292bffc4 HTMX -> htmx 2022-01-07 16:02:01 -05:00
86defea3c1 Add scripts to README
Also prep for release
2022-01-06 21:31:36 -05:00
9a9f159cab Add script functions
Also bump version to match htmx
2022-01-04 12:54:36 -05:00
19 changed files with 1457 additions and 917 deletions

View File

@@ -63,6 +63,8 @@ Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these
] ]
``` ```
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.
## Feedback / Help ## Feedback / Help
The author hangs out in the #htmx-general channel of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/). The author hangs out in the #htmx-general channel of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/).

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1 @@
module Program = let [<EntryPoint>] main _ = 0

35
src/Common.Tests/Tests.fs Normal file
View File

@@ -0,0 +1,35 @@
module Tests
open Giraffe.Htmx
open Xunit
/// Tests for the HxSwap module
module Swap =
[<Fact>]
let ``InnerHtml is correct`` () =
Assert.Equal ("innerHTML", HxSwap.InnerHtml)
[<Fact>]
let ``OuterHtml is correct`` () =
Assert.Equal ("outerHTML", HxSwap.OuterHtml)
[<Fact>]
let ``BeforeBegin is correct`` () =
Assert.Equal ("beforebegin", HxSwap.BeforeBegin)
[<Fact>]
let ``BeforeEnd is correct`` () =
Assert.Equal ("beforeend", HxSwap.BeforeEnd)
[<Fact>]
let ``AfterBegin is correct`` () =
Assert.Equal ("afterbegin", HxSwap.AfterBegin)
[<Fact>]
let ``AfterEnd is correct`` () =
Assert.Equal ("afterend", HxSwap.AfterEnd)
[<Fact>]
let ``None is correct`` () =
Assert.Equal ("none", HxSwap.None)

28
src/Common/Common.fs Normal file
View File

@@ -0,0 +1,28 @@
/// Common definitions shared between attribute values and response headers
[<AutoOpen>]
module Giraffe.Htmx.Common
/// Valid values for the `hx-swap` attribute / `HX-Reswap` header (may be combined with swap/settle/scroll/show config)
[<RequireQualifiedAccess>]
module HxSwap =
/// The default, replace the inner html of the target element
let InnerHtml = "innerHTML"
/// Replace the entire target element with the response
let OuterHtml = "outerHTML"
/// Insert the response before the target element
let BeforeBegin = "beforebegin"
/// Insert the response before the first child of the target element
let AfterBegin = "afterbegin"
/// Insert the response after the last child of the target element
let BeforeEnd = "beforeend"
/// Insert the response after the target element
let AfterEnd = "afterend"
/// Does not append content from response (out of band items will still be processed).
let None = "none"

View File

@@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Common definitions for Giraffe.Htmx</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Common.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>

5
src/Common/README.md Normal file
View File

@@ -0,0 +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.
**htmx version: 1.8.4**

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<VersionPrefix>0.9.3</VersionPrefix> <TargetFramework>net6.0</TargetFramework>
<PackageReleaseNotes>Add support for HX-Retarget header (added in htmx 1.6.1)</PackageReleaseNotes> <VersionPrefix>1.8.4</VersionPrefix>
<PackageReleaseNotes>Support new hx-validate attribute in htmx 1.8.1; add support for fragment rendering</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl> <PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl>

52
src/Giraffe.Htmx.sln Normal file
View File

@@ -0,0 +1,52 @@

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.Htmx.Tests", "Htmx.Tests\Giraffe.Htmx.Tests.fsproj", "{D7CDD578-7A6F-4EF6-846A-80A55037E049}"
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.ViewEngine.Htmx.Tests", "ViewEngine.Htmx.Tests\Giraffe.ViewEngine.Htmx.Tests.fsproj", "{F21C28CE-1F18-4CB0-B2F7-10DABE84FB78}"
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}") = "Giraffe.Htmx.Common.Tests", "Common.Tests\Giraffe.Htmx.Common.Tests.fsproj", "{E261A653-68D5-4D7B-99A4-F09282B50F8A}"
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
{D7CDD578-7A6F-4EF6-846A-80A55037E049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D7CDD578-7A6F-4EF6-846A-80A55037E049}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D7CDD578-7A6F-4EF6-846A-80A55037E049}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D7CDD578-7A6F-4EF6-846A-80A55037E049}.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
{F21C28CE-1F18-4CB0-B2F7-10DABE84FB78}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F21C28CE-1F18-4CB0-B2F7-10DABE84FB78}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F21C28CE-1F18-4CB0-B2F7-10DABE84FB78}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F21C28CE-1F18-4CB0-B2F7-10DABE84FB78}.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
{E261A653-68D5-4D7B-99A4-F09282B50F8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E261A653-68D5-4D7B-99A4-F09282B50F8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E261A653-68D5-4D7B-99A4-F09282B50F8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E261A653-68D5-4D7B-99A4-F09282B50F8A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -1,8 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -207,14 +207,25 @@ module HandlerTests =
let next (ctx : HttpContext) = Task.FromResult (Some ctx) let next (ctx : HttpContext) = Task.FromResult (Some ctx)
[<Fact>] [<Fact>]
let ``withHxPush succeeds`` () = let ``withHxPushUrl succeeds`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task { task {
let! _ = withHxPush "/a-new-url" next ctx let! _ = withHxPushUrl "/a-new-url" next ctx
Assert.True (dic.ContainsKey "HX-Push") Assert.True (dic.ContainsKey "HX-Push-Url")
Assert.Equal ("/a-new-url", dic.["HX-Push"].[0]) Assert.Equal ("/a-new-url", dic["HX-Push-Url"][0])
}
[<Fact>]
let ``withHxNoPushUrl succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxNoPushUrl next ctx
Assert.True (dic.ContainsKey "HX-Push-Url")
Assert.Equal ("false", dic["HX-Push-Url"][0])
} }
[<Fact>] [<Fact>]
@@ -225,7 +236,7 @@ module HandlerTests =
task { task {
let! _ = withHxRedirect "/somewhere-else" next ctx let! _ = withHxRedirect "/somewhere-else" next ctx
Assert.True (dic.ContainsKey "HX-Redirect") Assert.True (dic.ContainsKey "HX-Redirect")
Assert.Equal ("/somewhere-else", dic.["HX-Redirect"].[0]) Assert.Equal ("/somewhere-else", dic["HX-Redirect"][0])
} }
[<Fact>] [<Fact>]
@@ -236,7 +247,7 @@ module HandlerTests =
task { task {
let! _ = withHxRefresh true next ctx let! _ = withHxRefresh true next ctx
Assert.True (dic.ContainsKey "HX-Refresh") Assert.True (dic.ContainsKey "HX-Refresh")
Assert.Equal ("true", dic.["HX-Refresh"].[0]) Assert.Equal ("true", dic["HX-Refresh"][0])
} }
[<Fact>] [<Fact>]
@@ -247,7 +258,40 @@ module HandlerTests =
task { task {
let! _ = withHxRefresh false next ctx let! _ = withHxRefresh false next ctx
Assert.True (dic.ContainsKey "HX-Refresh") Assert.True (dic.ContainsKey "HX-Refresh")
Assert.Equal ("false", dic.["HX-Refresh"].[0]) Assert.Equal ("false", dic["HX-Refresh"][0])
}
[<Fact>]
let ``withHxReplaceUrl succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
Assert.True (dic.ContainsKey "HX-Replace-Url")
Assert.Equal ("/a-substitute-url", dic["HX-Replace-Url"][0])
}
[<Fact>]
let ``withHxNoReplaceUrl succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxNoReplaceUrl next ctx
Assert.True (dic.ContainsKey "HX-Replace-Url")
Assert.Equal ("false", dic["HX-Replace-Url"][0])
}
[<Fact>]
let ``withHxReswap succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Assert.True (dic.ContainsKey "HX-Reswap")
Assert.Equal (HxSwap.BeforeEnd, dic["HX-Reswap"][0])
} }
[<Fact>] [<Fact>]
@@ -258,7 +302,7 @@ module HandlerTests =
task { task {
let! _ = withHxRetarget "#somewhereElse" next ctx let! _ = withHxRetarget "#somewhereElse" next ctx
Assert.True (dic.ContainsKey "HX-Retarget") Assert.True (dic.ContainsKey "HX-Retarget")
Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0]) Assert.Equal ("#somewhereElse", dic["HX-Retarget"][0])
} }
[<Fact>] [<Fact>]
@@ -269,7 +313,7 @@ module HandlerTests =
task { task {
let! _ = withHxTrigger "doSomething" next ctx let! _ = withHxTrigger "doSomething" next ctx
Assert.True (dic.ContainsKey "HX-Trigger") Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("doSomething", dic.["HX-Trigger"].[0]) Assert.Equal ("doSomething", dic["HX-Trigger"][0])
} }
[<Fact>] [<Fact>]
@@ -280,7 +324,7 @@ module HandlerTests =
task { task {
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger") Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic.["HX-Trigger"].[0]) Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic["HX-Trigger"][0])
} }
[<Fact>] [<Fact>]
@@ -291,7 +335,7 @@ module HandlerTests =
task { task {
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("byTheWay", dic.["HX-Trigger-After-Settle"].[0]) Assert.Equal ("byTheWay", dic["HX-Trigger-After-Settle"][0])
} }
[<Fact>] [<Fact>]
@@ -302,7 +346,7 @@ module HandlerTests =
task { task {
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic.["HX-Trigger-After-Settle"].[0]) Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic["HX-Trigger-After-Settle"][0])
} }
[<Fact>] [<Fact>]
@@ -313,7 +357,7 @@ module HandlerTests =
task { task {
let! _ = withHxTriggerAfterSwap "justASec" next ctx let! _ = withHxTriggerAfterSwap "justASec" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
Assert.Equal ("justASec", dic.["HX-Trigger-After-Swap"].[0]) Assert.Equal ("justASec", dic["HX-Trigger-After-Swap"][0])
} }
[<Fact>] [<Fact>]
@@ -324,6 +368,5 @@ module HandlerTests =
task { task {
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
Assert.Equal ("""{ "this": "1", "that": "2" }""", dic.["HX-Trigger-After-Swap"].[0]) Assert.Equal ("""{ "this": "1", "that": "2" }""", dic["HX-Trigger-After-Swap"][0])
} }

View File

@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>htmx header extensions and helpers for Giraffe</Description> <Description>htmx header extensions and helpers for Giraffe</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
@@ -16,4 +15,8 @@
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" Version="5.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -6,7 +6,7 @@ open System
/// Determine if the given header is present /// Determine if the given header is present
let private hdr (headers : IHeaderDictionary) hdr = let private hdr (headers : IHeaderDictionary) hdr =
match headers.[hdr] with it when it = StringValues.Empty -> None | it -> Some it.[0] match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0]
/// Extensions to the header dictionary /// Extensions to the header dictionary
type IHeaderDictionary with type IHeaderDictionary with
@@ -39,10 +39,10 @@ type IHeaderDictionary with
/// Extensions for the request object /// Extensions for the request object
type HttpRequest with type HttpRequest with
/// Whether this request was initiated from HTMX /// Whether this request was initiated from htmx
member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false
/// Whether this request is an HTMX history-miss refresh request /// Whether this request is an htmx history-miss refresh request
member this.IsHtmxRefresh with get () = member this.IsHtmxRefresh with get () =
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false) this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
@@ -62,9 +62,21 @@ module Handlers =
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
// Pushes a new url into the history stack /// Pushes a new url into the history stack
let withHxPush : string -> HttpHandler = let withHxPushUrl : string -> HttpHandler =
setHttpHeader "HX-Push" setHttpHeader "HX-Push-Url"
/// Explicitly do not push a new URL into the history stack
let withHxNoPushUrl : HttpHandler =
toLowerBool false |> withHxPushUrl
/// Pushes a new url into the history stack
[<Obsolete "Use withHxPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
let withHxPush = withHxPushUrl
/// Explicitly do not push a new URL into the history stack
[<Obsolete "Use withHxNoPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
let withHxNoPush = withHxNoPushUrl
/// Can be used to do a client-side redirect to a new location /// Can be used to do a client-side redirect to a new location
let withHxRedirect : string -> HttpHandler = let withHxRedirect : string -> HttpHandler =
@@ -74,6 +86,18 @@ module Handlers =
let withHxRefresh : bool -> HttpHandler = let withHxRefresh : bool -> HttpHandler =
toLowerBool >> setHttpHeader "HX-Refresh" toLowerBool >> setHttpHeader "HX-Refresh"
/// Replaces the current URL in the history stack
let withHxReplaceUrl : string -> HttpHandler =
setHttpHeader "HX-Replace-Url"
/// Explicitly do not replace the current URL in the history stack
let withHxNoReplaceUrl : HttpHandler =
toLowerBool false |> withHxReplaceUrl
/// Override the `hx-swap` attribute from the initiating element
let withHxReswap : string -> HttpHandler =
setHttpHeader "HX-Reswap"
/// Allows you to override the `hx-target` attribute /// Allows you to override the `hx-target` attribute
let withHxRetarget : string -> HttpHandler = let withHxRetarget : string -> HttpHandler =
setHttpHeader "HX-Retarget" setHttpHeader "HX-Retarget"

View File

@@ -2,6 +2,8 @@
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`. This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 1.8.4**
### Setup ### Setup
1. Install the package. 1. Install the package.
@@ -25,9 +27,11 @@ To set a response header:
let myHandler : HttpHander = let myHandler : HttpHander =
fun next ctx -> fun next ctx ->
// some meaningful work // some meaningful work
withHxPush "/some/new/url" >=> [other handlers] withHxPushUrl "/some/new/url" >=> [other handlers]
``` ```
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.
### Learn ### 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. 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.

View File

@@ -1,8 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable> <IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile> <GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup> </PropertyGroup>

View File

@@ -101,38 +101,6 @@ module Request =
Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false) Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false)
/// Tests for the HxSwap module
module Swap =
[<Fact>]
let ``InnerHtml is correct`` () =
Assert.Equal ("innerHTML", HxSwap.InnerHtml)
[<Fact>]
let ``OuterHtml is correct`` () =
Assert.Equal ("outerHTML", HxSwap.OuterHtml)
[<Fact>]
let ``BeforeBegin is correct`` () =
Assert.Equal ("beforebegin", HxSwap.BeforeBegin)
[<Fact>]
let ``BeforeEnd is correct`` () =
Assert.Equal ("beforeend", HxSwap.BeforeEnd)
[<Fact>]
let ``AfterBegin is correct`` () =
Assert.Equal ("afterbegin", HxSwap.AfterBegin)
[<Fact>]
let ``AfterEnd is correct`` () =
Assert.Equal ("afterend", HxSwap.AfterEnd)
[<Fact>]
let ``None is correct`` () =
Assert.Equal ("none", HxSwap.None)
/// Tests for the HxTrigger module /// Tests for the HxTrigger module
module Trigger = module Trigger =
@@ -344,6 +312,10 @@ module Attributes =
let ``_hxDisable succeeds`` () = let ``_hxDisable succeeds`` () =
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>""" p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
[<Fact>]
let ``_hxDisinherit succeeds`` () =
strong [ _hxDisinherit "*" ] [] |> shouldRender """<strong hx-disinherit="*"></strong>"""
[<Fact>] [<Fact>]
let ``_hxEncoding succeeds`` () = let ``_hxEncoding succeeds`` () =
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>""" form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
@@ -399,12 +371,16 @@ module Attributes =
[<Fact>] [<Fact>]
let ``_hxPushUrl succeeds`` () = let ``_hxPushUrl succeeds`` () =
dl [ _hxPushUrl ] [] |> shouldRender """<dl hx-push-url></dl>""" dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """<dl hx-push-url="/a-b-c"></dl>"""
[<Fact>] [<Fact>]
let ``_hxPut succeeds`` () = let ``_hxPut succeeds`` () =
s [ _hxPut "/take-this" ] [] |> shouldRender """<s hx-put="/take-this"></s>""" s [ _hxPut "/take-this" ] [] |> shouldRender """<s hx-put="/take-this"></s>"""
[<Fact>]
let ``_hxReplaceUrl succeeds`` () =
p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """<p hx-replace-url="/something-else"></p>"""
[<Fact>] [<Fact>]
let ``_hxRequest succeeds`` () = let ``_hxRequest succeeds`` () =
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>""" u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
@@ -413,6 +389,10 @@ module Attributes =
let ``_hxSelect succeeds`` () = let ``_hxSelect succeeds`` () =
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>""" nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
[<Fact>]
let ``_hxSelectOob succeeds`` () =
section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
[<Fact>] [<Fact>]
let ``_hxSse succeeds`` () = let ``_hxSse succeeds`` () =
footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """<footer hx-sse="connect:/my-events"></footer>""" footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """<footer hx-sse="connect:/my-events"></footer>"""
@@ -425,6 +405,10 @@ module Attributes =
let ``_hxSwapOob succeeds`` () = let ``_hxSwapOob succeeds`` () =
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>""" li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
[<Fact>]
let ``_hxSync succeeds`` () =
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
[<Fact>] [<Fact>]
let ``_hxTarget succeeds`` () = let ``_hxTarget succeeds`` () =
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>""" header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
@@ -442,3 +426,154 @@ module Attributes =
let ``_hxWs succeeds`` () = let ``_hxWs succeeds`` () =
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>""" ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""
/// Tests for the Script module
module Script =
[<Fact>]
let ``Script.minified succeeds`` () =
let html = RenderView.AsString.htmlNode Script.minified
Assert.Equal
("""<script src="https://unpkg.com/htmx.org@1.8.4" integrity="sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" crossorigin="anonymous"></script>""",
html)
[<Fact>]
let ``Script.unminified succeeds`` () =
let html = RenderView.AsString.htmlNode Script.unminified
Assert.Equal
("""<script src="https://unpkg.com/htmx.org@1.8.4/dist/htmx.js" integrity="sha384-sh63gh7zpjxu153RyKJ06Oy5HxIVl6cchze/dJOHulOI7u0sGZoC/CfQJHPODhFn" crossorigin="anonymous"></script>""",
html)
/// Tests for the RenderFragment module
module RenderFragment =
open System.Text
[<Fact>]
let ``RenderFragment.findIdNode fails with a Text node`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "blue" (Text "")))
[<Fact>]
let ``RenderFragment.findIdNode fails with a VoidElement without a matching ID`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "purple" (br [ _id "mauve" ])))
[<Fact>]
let ``RenderFragment.findIdNode fails with a ParentNode with no children with a matching ID`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "green" (p [] [ str "howdy"; span [] [ str "huh" ] ])))
[<Fact>]
let ``RenderFragment.findIdNode succeeds with a VoidElement with a matching ID`` () =
let leNode = hr [ _id "groovy" ]
let foundNode = RenderFragment.findIdNode "groovy" leNode
Assert.True (Option.isSome foundNode)
Assert.Same (leNode, foundNode.Value)
[<Fact>]
let ``RenderFragment.findIdNode succeeds with a ParentNode with a child with a matching ID`` () =
let leNode = span [ _id "its-me" ] [ str "Mario" ]
let foundNode = RenderFragment.findIdNode "its-me" (p [] [ str "test"; str "again"; leNode; str "un mas" ])
Assert.True (Option.isSome foundNode)
Assert.Same (leNode, foundNode.Value)
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// Tests for the AsString module
module AsString =
[<Fact>]
let ``RenderFragment.AsString.htmlFromNodes succeeds when an ID is matched`` () =
let html =
RenderFragment.AsString.htmlFromNodes "needle"
[ p [] []; p [ _id "haystack" ] [ span [ _id "needle" ] [ str "ouch" ]; str "hay"; str "hay" ]]
Assert.Equal ("""<span id="needle">ouch</span>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNodes fails when an ID is not matched`` () =
Assert.Equal (nodeNotFound "oops", RenderFragment.AsString.htmlFromNodes "oops" [])
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode succeeds when ID is matched at top level`` () =
let html = RenderFragment.AsString.htmlFromNode "wow" (p [ _id "wow" ] [ str "found it" ])
Assert.Equal ("""<p id="wow">found it</p>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode succeeds when ID is matched in child element`` () =
let html =
div [] [ p [] [ str "not it" ]; p [ _id "hey" ] [ str "ta-da" ]]
|> RenderFragment.AsString.htmlFromNode "hey"
Assert.Equal ("""<p id="hey">ta-da</p>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode fails when an ID is not matched`` () =
Assert.Equal (nodeNotFound "me", RenderFragment.AsString.htmlFromNode "me" (hr []))
/// Tests for the AsBytes module
module AsBytes =
/// Alias for UTF-8 encoding
let private utf8 = Encoding.UTF8
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNodes succeeds when an ID is matched`` () =
let bytes =
RenderFragment.AsBytes.htmlFromNodes "found"
[ p [] []; p [ _id "not-it" ] [ str "nope"; span [ _id "found" ] [ str "boo" ]; str "nope" ]]
Assert.Equal<byte> (utf8.GetBytes """<span id="found">boo</span>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNodes fails when an ID is not matched`` () =
Assert.Equal<byte> (utf8.GetBytes (nodeNotFound "whiff"), RenderFragment.AsBytes.htmlFromNodes "whiff" [])
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode succeeds when ID is matched at top level`` () =
let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ])
Assert.Equal<byte> (utf8.GetBytes """<p id="first">!!!</p>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode succeeds when ID is matched in child element`` () =
let bytes =
div [] [ p [] [ str "not me" ]; p [ _id "child" ] [ str "node" ]]
|> RenderFragment.AsBytes.htmlFromNode "child"
Assert.Equal<byte> (utf8.GetBytes """<p id="child">node</p>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode fails when an ID is not matched`` () =
Assert.Equal<byte> (utf8.GetBytes (nodeNotFound "foo"), RenderFragment.AsBytes.htmlFromNode "foo" (hr []))
/// Tests for the IntoStringBuilder module
module IntoStringBuilder =
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNodes succeeds when an ID is matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me"
[ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]]
Assert.Equal ("""<span id="find-me">;)</span>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNodes fails when an ID is not matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" []
Assert.Equal (nodeNotFound "missing", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode succeeds when ID is matched at top level`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ])
Assert.Equal ("""<p id="top">pinnacle</p>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode succeeds when ID is matched in child element`` () =
let sb = StringBuilder ()
div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]]
|> RenderFragment.IntoStringBuilder.htmlFromNode sb "it"
Assert.Equal ("""<p id="it">is here</p>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode fails when an ID is not matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr [])
Assert.Equal (nodeNotFound "bar", string sb)

View File

@@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description> <Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
@@ -16,4 +15,8 @@
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,4 +1,4 @@
module Giraffe.ViewEngine.Htmx module Giraffe.ViewEngine.Htmx
/// Serialize a list of key/value pairs to JSON (very rudimentary) /// Serialize a list of key/value pairs to JSON (very rudimentary)
let private toJson (kvps : (string * string) list) = let private toJson (kvps : (string * string) list) =
@@ -11,8 +11,10 @@ let private toJson (kvps : (string * string) list) =
/// Valid values for the `hx-encoding` attribute /// Valid values for the `hx-encoding` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxEncoding = module HxEncoding =
/// A standard HTTP form /// A standard HTTP form
let Form = "application/x-www-form-urlencoded" let Form = "application/x-www-form-urlencoded"
/// A multipart form (used for file uploads) /// A multipart form (used for file uploads)
let MultipartForm = "multipart/form-data" let MultipartForm = "multipart/form-data"
@@ -20,6 +22,7 @@ module HxEncoding =
/// Helper to create the `hx-headers` attribute /// Helper to create the `hx-headers` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxHeaders = module HxHeaders =
/// Create headers from a list of key/value pairs /// Create headers from a list of key/value pairs
let From = toJson let From = toJson
@@ -27,12 +30,16 @@ module HxHeaders =
/// Values / helpers for the `hx-params` attribute /// Values / helpers for the `hx-params` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxParams = module HxParams =
/// Include all parameters /// Include all parameters
let All = "*" let All = "*"
/// Include no parameters /// Include no parameters
let None = "none" let None = "none"
/// Include the specified parameters /// Include the specified parameters
let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
/// Exclude the specified parameters /// Exclude the specified parameters
let Except fields = With fields |> sprintf "not %s" let Except fields = With fields |> sprintf "not %s"
@@ -40,108 +47,124 @@ module HxParams =
/// Helpers to define `hx-request` attribute values /// Helpers to define `hx-request` attribute values
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxRequest = module HxRequest =
/// Convert a boolean to its lowercase string equivalent /// Convert a boolean to its lowercase string equivalent
let private toLowerBool (it : bool) = let private toLowerBool (it : bool) =
(string it).ToLowerInvariant () (string it).ToLowerInvariant ()
/// Configure the request with various options /// Configure the request with various options
let Configure (opts : string list) = let Configure (opts : string list) =
opts opts
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
/// Set a timeout (in milliseconds) /// Set a timeout (in milliseconds)
let Timeout (ms : int) = $"\"timeout\": {ms}" let Timeout (ms : int) = $"\"timeout\": {ms}"
/// Include or exclude credentials from the request /// Include or exclude credentials from the request
let Credentials = toLowerBool >> sprintf "\"credentials\": %s" let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
/// Exclude or include headers from the request /// Exclude or include headers from the request
let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s" let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config)
[<RequireQualifiedAccess>]
module HxSwap =
/// The default, replace the inner html of the target element
let InnerHtml = "innerHTML"
/// Replace the entire target element with the response
let OuterHtml = "outerHTML"
/// Insert the response before the target element
let BeforeBegin = "beforebegin"
/// Insert the response before the first child of the target element
let AfterBegin = "afterbegin"
/// Insert the response after the last child of the target element
let BeforeEnd = "beforeend"
/// Insert the response after the target element
let AfterEnd = "afterend"
/// Does not append content from response (out of band items will still be processed).
let None = "none"
/// Helpers for the `hx-trigger` attribute /// Helpers for the `hx-trigger` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxTrigger = module HxTrigger =
/// Append a filter to a trigger /// Append a filter to a trigger
let private appendFilter filter (trigger : string) = let private appendFilter filter (trigger : string) =
match trigger.Contains "[" with match trigger.Contains "[" with
| true -> | true ->
let parts = trigger.Split ('[', ']') let parts = trigger.Split ('[', ']')
$"{parts.[0]}[{parts.[1]}&&{filter}]" $"{parts[0]}[{parts[1]}&&{filter}]"
| false -> $"{trigger}[{filter}]" | false -> $"{trigger}[{filter}]"
/// Trigger the event on a click /// Trigger the event on a click
let Click = "click" let Click = "click"
/// Trigger the event on page load /// Trigger the event on page load
let Load = "load" let Load = "load"
/// Trigger the event when the item is visible /// Trigger the event when the item is visible
let Revealed = "revealed" let Revealed = "revealed"
/// Trigger this event every [timing declaration] /// Trigger this event every [timing declaration]
let Every (duration : string) = $"every {duration}" let Every (duration : string) = $"every {duration}"
/// Helpers for defining filters /// Helpers for defining filters
module Filter = module Filter =
/// Only trigger the event if the `ALT` key is pressed /// Only trigger the event if the `ALT` key is pressed
let Alt = appendFilter "altKey" let Alt = appendFilter "altKey"
/// Only trigger the event if the `CTRL` key is pressed /// Only trigger the event if the `CTRL` key is pressed
let Ctrl = appendFilter "ctrlKey" let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if the `SHIFT` key is pressed /// Only trigger the event if the `SHIFT` key is pressed
let Shift = appendFilter "shiftKey" let Shift = appendFilter "shiftKey"
/// Only trigger the event if `CTRL+ALT` are pressed /// Only trigger the event if `CTRL+ALT` are pressed
let CtrlAlt = Ctrl >> Alt let CtrlAlt = Ctrl >> Alt
/// Only trigger the event if `CTRL+SHIFT` are pressed /// Only trigger the event if `CTRL+SHIFT` are pressed
let CtrlShift = Ctrl >> Shift let CtrlShift = Ctrl >> Shift
/// Only trigger the event if `CTRL+ALT+SHIFT` are pressed /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed
let CtrlAltShift = CtrlAlt >> Shift let CtrlAltShift = CtrlAlt >> Shift
/// Only trigger the event if `ALT+SHIFT` are pressed /// Only trigger the event if `ALT+SHIFT` are pressed
let AltShift = Alt >> Shift let AltShift = Alt >> Shift
/// Append a modifier to the current trigger /// Append a modifier to the current trigger
let private appendModifier modifier current = let private appendModifier modifier current =
match current with "" -> modifier | _ -> $"{current} {modifier}" if current = "" then modifier else $"{current} {modifier}"
/// Only trigger once /// Only trigger once
let Once = appendModifier "once" let Once = appendModifier "once"
/// Trigger when changed /// Trigger when changed
let Changed = appendModifier "changed" let Changed = appendModifier "changed"
/// Delay execution; resets every time the event is seen /// Delay execution; resets every time the event is seen
let Delay = sprintf "delay:%s" >> appendModifier let Delay = sprintf "delay:%s" >> appendModifier
/// Throttle execution; ignore other events, fire when duration passes /// Throttle execution; ignore other events, fire when duration passes
let Throttle = sprintf "throttle:%s" >> appendModifier let Throttle = sprintf "throttle:%s" >> appendModifier
/// Trigger this event from a CSS selector /// Trigger this event from a CSS selector
let From = sprintf "from:%s" >> appendModifier let From = sprintf "from:%s" >> appendModifier
/// Trigger this event from the `document` object /// Trigger this event from the `document` object
let FromDocument = From "document" let FromDocument = From "document"
/// Trigger this event from the `window` object /// Trigger this event from the `window` object
let FromWindow = From "window" let FromWindow = From "window"
/// Trigger this event from the closest parent CSS selector /// Trigger this event from the closest parent CSS selector
let FromClosest = sprintf "closest %s" >> From let FromClosest = sprintf "closest %s" >> From
/// Trigger this event from the closest child CSS selector /// Trigger this event from the closest child CSS selector
let FromFind = sprintf "find %s" >> From let FromFind = sprintf "find %s" >> From
/// Target the given CSS selector with the results of this event /// Target the given CSS selector with the results of this event
let Target = sprintf "target:%s" >> appendModifier let Target = sprintf "target:%s" >> appendModifier
/// Prevent any further events from occurring after this one fires /// Prevent any further events from occurring after this one fires
let Consume = appendModifier "consume" let Consume = appendModifier "consume"
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" /// Configure queueing when events fire when others are in flight; if unspecified, the default is "last"
let Queue = sprintf "queue:%s" >> appendModifier let Queue = sprintf "queue:%s" >> appendModifier
/// Queue the first event, discard all others (i.e., a FIFO queue of 1) /// Queue the first event, discard all others (i.e., a FIFO queue of 1)
let QueueFirst = Queue "first" let QueueFirst = Queue "first"
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
let QueueLast = Queue "last" let QueueLast = Queue "last"
/// Queue all events; discard none /// Queue all events; discard none
let QueueAll = Queue "all" let QueueAll = Queue "all"
/// Queue no events; discard all /// Queue no events; discard all
let QueueNone = Queue "none" let QueueNone = Queue "none"
@@ -149,66 +172,201 @@ module HxTrigger =
/// Helper to create the `hx-vals` attribute /// Helper to create the `hx-vals` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxVals = module HxVals =
/// Create values from a list of key/value pairs /// Create values from a list of key/value pairs
let From = toJson let From = toJson
/// Attributes and flags for HTMX /// Attributes and flags for htmx
[<AutoOpen>] [<AutoOpen>]
module HtmxAttrs = module HtmxAttrs =
/// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false) /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
let _hxBoost = attr "hx-boost" "true" let _hxBoost = attr "hx-boost" "true"
/// Shows a confirm() dialog before issuing a request /// Shows a confirm() dialog before issuing a request
let _hxConfirm = attr "hx-confirm" let _hxConfirm = attr "hx-confirm"
/// Issues a DELETE to the specified URL /// Issues a DELETE to the specified URL
let _hxDelete = attr "hx-delete" let _hxDelete = attr "hx-delete"
/// Disables htmx processing for the given node and any children nodes /// Disables htmx processing for the given node and any children nodes
let _hxDisable = flag "hx-disable" let _hxDisable = flag "hx-disable"
/// Disinherit all ("*") or specific htmx attributes
let _hxDisinherit = attr "hx-disinherit"
/// Changes the request encoding type /// Changes the request encoding type
let _hxEncoding = attr "hx-encoding" let _hxEncoding = attr "hx-encoding"
/// Extensions to use for this element /// Extensions to use for this element
let _hxExt = attr "hx-ext" let _hxExt = attr "hx-ext"
/// Issues a GET to the specified URL /// Issues a GET to the specified URL
let _hxGet = attr "hx-get" let _hxGet = attr "hx-get"
/// Adds to the headers that will be submitted with the request /// Adds to the headers that will be submitted with the request
let _hxHeaders = attr "hx-headers" let _hxHeaders = attr "hx-headers"
/// The element to snapshot and restore during history navigation /// The element to snapshot and restore during history navigation
let _hxHistoryElt = flag "hx-history-elt" let _hxHistoryElt = flag "hx-history-elt"
/// Includes additional data in AJAX requests /// Includes additional data in AJAX requests
let _hxInclude = attr "hx-include" let _hxInclude = attr "hx-include"
/// The element to put the htmx-request class on during the AJAX request /// The element to put the htmx-request class on during the AJAX request
let _hxIndicator = attr "hx-indicator" let _hxIndicator = attr "hx-indicator"
/// Overrides a previous `hx-boost` /// Overrides a previous `hx-boost`
let _hxNoBoost = attr "hx-boost" "false" let _hxNoBoost = attr "hx-boost" "false"
/// Filters the parameters that will be submitted with a request /// Filters the parameters that will be submitted with a request
let _hxParams = attr "hx-params" let _hxParams = attr "hx-params"
/// Issues a PATCH to the specified URL /// Issues a PATCH to the specified URL
let _hxPatch = attr "hx-patch" let _hxPatch = attr "hx-patch"
/// Issues a POST to the specified URL /// Issues a POST to the specified URL
let _hxPost = attr "hx-post" let _hxPost = attr "hx-post"
/// Preserves an element between requests /// Preserves an element between requests
let _hxPreserve = attr "hx-preserve" "true" let _hxPreserve = attr "hx-preserve" "true"
/// Shows a prompt before submitting a request /// Shows a prompt before submitting a request
let _hxPrompt = attr "hx-prompt" let _hxPrompt = attr "hx-prompt"
/// Pushes the URL into the location bar, creating a new history entry /// Pushes the URL into the location bar, creating a new history entry
let _hxPushUrl = flag "hx-push-url" let _hxPushUrl = attr "hx-push-url"
/// Issues a PUT to the specified URL /// Issues a PUT to the specified URL
let _hxPut = attr "hx-put" let _hxPut = attr "hx-put"
/// Replaces the current URL in the browser's history stack
let _hxReplaceUrl = attr "hx-replace-url"
/// Configures various aspects of the request /// Configures various aspects of the request
let _hxRequest = attr "hx-request" let _hxRequest = attr "hx-request"
/// Selects a subset of the server response to process /// Selects a subset of the server response to process
let _hxSelect = attr "hx-select" let _hxSelect = attr "hx-select"
/// Selects a subset of an out-of-band server response
let _hxSelectOob = attr "hx-select-oob"
/// Establishes and listens to Server Sent Event (SSE) sources for events /// Establishes and listens to Server Sent Event (SSE) sources for events
let _hxSse = attr "hx-sse" let _hxSse = attr "hx-sse"
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd') /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd')
let _hxSwap = attr "hx-swap" let _hxSwap = attr "hx-swap"
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
let _hxSwapOob = attr "hx-swap-oob" let _hxSwapOob = attr "hx-swap-oob"
/// Synchronize events based on another element
let _hxSync = attr "hx-sync"
/// Specifies the target element to be swapped /// Specifies the target element to be swapped
let _hxTarget = attr "hx-target" let _hxTarget = attr "hx-target"
/// Specifies the event that triggers the request /// Specifies the event that triggers the request
let _hxTrigger = attr "hx-trigger" let _hxTrigger = attr "hx-trigger"
/// Validate an input element (uses HTML5 validation API)
let _hxValidate = flag "hx-validate"
/// Adds to the parameters that will be submitted with the request /// Adds to the parameters that will be submitted with the request
let _hxVals = attr "hx-vals" let _hxVals = attr "hx-vals"
/// Establishes a WebSocket or sends information to one /// Establishes a WebSocket or sends information to one
let _hxWs = attr "hx-ws" let _hxWs = attr "hx-ws"
/// Script tags to pull htmx into an web page
module Script =
/// Script tag to load the minified version from unpkg.com
let minified =
script [ _src "https://unpkg.com/htmx.org@1.8.4"
_integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
_crossorigin "anonymous" ] []
/// Script tag to load the unminified version from unpkg.com
let unminified =
script [ _src "https://unpkg.com/htmx.org@1.8.4/dist/htmx.js"
_integrity "sha384-sh63gh7zpjxu153RyKJ06Oy5HxIVl6cchze/dJOHulOI7u0sGZoC/CfQJHPODhFn"
_crossorigin "anonymous" ] []
/// Functions to extract and render an HTML fragment from a document
[<RequireQualifiedAccess>]
module RenderFragment =
/// Does this element have an ID matching the requested ID name?
let private isIdElement nodeId (elt : XmlElement) =
snd elt
|> Array.exists (fun attr ->
match attr with
| KeyValue (name, value) -> name = "id" && value = nodeId
| Boolean _ -> false)
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// Find the node with the named ID
let rec findIdNode nodeId (node : XmlNode) : XmlNode option =
match node with
| Text _ -> None
| 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 (fun c -> findIdNode nodeId c)
/// Functions to render a fragment as a string
[<RequireQualifiedAccess>]
module AsString =
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Functions to render a fragment as bytes
[<RequireQualifiedAccess>]
module AsBytes =
let private utf8 = System.Text.Encoding.UTF8
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Functions to render a fragment into a StringBuilder
[<RequireQualifiedAccess>]
module IntoStringBuilder =
/// Render to HTML for the given ID
let htmlFromNodes sb nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore
/// Render to HTML for the given ID
let htmlFromNode sb nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore

View File

@@ -2,6 +2,8 @@
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine. This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
**htmx version: 1.8.4**
### Setup ### Setup
1. Install the package. 1. Install the package.
@@ -21,10 +23,14 @@ Support modules include:
- `HxHeaders` - `HxHeaders`
- `HxParams` - `HxParams`
- `HxRequest` - `HxRequest`
- `HxSwap` - `HxSwap` (requires `open Giraffe.Htmx`)
- `HxTrigger` - `HxTrigger`
- `HxVals` - `HxVals`
There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging).
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).
### Learn ### Learn
htmx's attributes and these attribute functions map one-to-one. The lone exception is `_hxBoost`, which implies `true`; use `_hxNoBoost` to set it to `false`. The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though: htmx's attributes and these attribute functions map one-to-one. The lone exception is `_hxBoost`, which implies `true`; use `_hxNoBoost` to set it to `false`. The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though: