5 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
18 changed files with 1438 additions and 968 deletions

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>1.7.0</VersionPrefix> <TargetFramework>net6.0</TargetFramework>
<PackageReleaseNotes>Support new attributes/headers in htmx 1.7.0</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

@@ -9,332 +9,364 @@ open Xunit
/// Tests for the IHeaderDictionary extension properties /// Tests for the IHeaderDictionary extension properties
module IHeaderDictionaryExtensions = module IHeaderDictionaryExtensions =
[<Fact>] [<Fact>]
let ``HxBoosted succeeds when the header is not present`` () = let ``HxBoosted succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True
[<Fact>] [<Fact>]
let ``HxBoosted succeeds when the header is present and true`` () = let ``HxBoosted succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "true") dic.Add ("HX-Boosted", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True
Option.get ctx.Request.Headers.HxBoosted |> Assert.True Option.get ctx.Request.Headers.HxBoosted |> Assert.True
[<Fact>] [<Fact>]
let ``HxBoosted succeeds when the header is present and false`` () = let ``HxBoosted succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "false") dic.Add ("HX-Boosted", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True
Option.get ctx.Request.Headers.HxBoosted |> Assert.False Option.get ctx.Request.Headers.HxBoosted |> Assert.False
[<Fact>] [<Fact>]
let ``HxCurrentUrl succeeds when the header is not present`` () = let ``HxCurrentUrl succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True
[<Fact>] [<Fact>]
let ``HxCurrentUrl succeeds when the header is present`` () = let ``HxCurrentUrl succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Current-URL", "http://localhost/test.htm") dic.Add ("HX-Current-URL", "http://localhost/test.htm")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True
Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl) Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl)
[<Fact>] [<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is not present`` () = let ``HxHistoryRestoreRequest succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
[<Fact>] [<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () = let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "true") dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
[<Fact>] [<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () = let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "false") dic.Add ("HX-History-Restore-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False
[<Fact>] [<Fact>]
let ``HxPrompt succeeds when the header is not present`` () = let ``HxPrompt succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True
[<Fact>] [<Fact>]
let ``HxPrompt succeeds when the header is present`` () = let ``HxPrompt succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Prompt", "of course") dic.Add ("HX-Prompt", "of course")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True
Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt) Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt)
[<Fact>] [<Fact>]
let ``HxRequest succeeds when the header is not present`` () = let ``HxRequest succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxRequest |> Assert.True Option.isNone ctx.Request.Headers.HxRequest |> Assert.True
[<Fact>] [<Fact>]
let ``HxRequest succeeds when the header is present and true`` () = let ``HxRequest succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxRequest |> Assert.True Option.isSome ctx.Request.Headers.HxRequest |> Assert.True
Option.get ctx.Request.Headers.HxRequest |> Assert.True Option.get ctx.Request.Headers.HxRequest |> Assert.True
[<Fact>] [<Fact>]
let ``HxRequest succeeds when the header is present and false`` () = let ``HxRequest succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Request", "false") dic.Add ("HX-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxRequest |> Assert.True Option.isSome ctx.Request.Headers.HxRequest |> Assert.True
Option.get ctx.Request.Headers.HxRequest |> Assert.False Option.get ctx.Request.Headers.HxRequest |> Assert.False
[<Fact>] [<Fact>]
let ``HxTarget succeeds when the header is not present`` () = let ``HxTarget succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTarget |> Assert.True Option.isNone ctx.Request.Headers.HxTarget |> Assert.True
[<Fact>] [<Fact>]
let ``HxTarget succeeds when the header is present`` () = let ``HxTarget succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Target", "#leItem") dic.Add ("HX-Target", "#leItem")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTarget |> Assert.True Option.isSome ctx.Request.Headers.HxTarget |> Assert.True
Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget) Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget)
[<Fact>] [<Fact>]
let ``HxTrigger succeeds when the header is not present`` () = let ``HxTrigger succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True
[<Fact>] [<Fact>]
let ``HxTrigger succeeds when the header is present`` () = let ``HxTrigger succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Trigger", "#trig") dic.Add ("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True
Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger) Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger)
[<Fact>] [<Fact>]
let ``HxTriggerName succeeds when the header is not present`` () = let ``HxTriggerName succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True
[<Fact>] [<Fact>]
let ``HxTriggerName succeeds when the header is present`` () = let ``HxTriggerName succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Trigger-Name", "click") dic.Add ("HX-Trigger-Name", "click")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True
Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName) Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName)
/// Tests for the HttpRequest extension properties /// Tests for the HttpRequest extension properties
module HttpRequestExtensions = module HttpRequestExtensions =
[<Fact>] [<Fact>]
let ``IsHtmx succeeds when request is not from htmx`` () = let ``IsHtmx succeeds when request is not from htmx`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Assert.False ctx.Request.IsHtmx Assert.False ctx.Request.IsHtmx
[<Fact>] [<Fact>]
let ``IsHtmx succeeds when request is from htmx`` () = let ``IsHtmx succeeds when request is from htmx`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.True ctx.Request.IsHtmx Assert.True ctx.Request.IsHtmx
[<Fact>] [<Fact>]
let ``IsHtmxRefresh succeeds when request is not from htmx`` () = let ``IsHtmxRefresh succeeds when request is not from htmx`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Assert.False ctx.Request.IsHtmxRefresh Assert.False ctx.Request.IsHtmxRefresh
[<Fact>] [<Fact>]
let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () = let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.False ctx.Request.IsHtmxRefresh Assert.False ctx.Request.IsHtmxRefresh
[<Fact>] [<Fact>]
let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () = let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () =
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary () let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true") dic.Add ("HX-Request", "true")
dic.Add ("HX-History-Restore-Request", "true") dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.True ctx.Request.IsHtmxRefresh Assert.True ctx.Request.IsHtmxRefresh
/// Tests for the HttpHandler functions provided in the Handlers module /// Tests for the HttpHandler functions provided in the Handlers module
module HandlerTests = module HandlerTests =
open System.Threading.Tasks open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate /// Dummy "next" parameter to get the pipeline to execute/terminate
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>] [<Fact>]
let ``withHxNoPush succeeds`` () = let ``withHxNoPushUrl 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! _ = withHxNoPush next ctx let! _ = withHxNoPushUrl next ctx
Assert.True (dic.ContainsKey "HX-Push") Assert.True (dic.ContainsKey "HX-Push-Url")
Assert.Equal ("false", dic.["HX-Push"].[0]) Assert.Equal ("false", dic["HX-Push-Url"][0])
} }
[<Fact>] [<Fact>]
let ``withHxRedirect succeeds`` () = let ``withHxRedirect 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! _ = 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>]
let ``withHxRefresh succeeds when set to true`` () = let ``withHxRefresh succeeds when set to true`` () =
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! _ = 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>]
let ``withHxRefresh succeeds when set to false`` () = let ``withHxRefresh succeeds when set to false`` () =
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! _ = 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>] [<Fact>]
let ``withHxRetarget succeeds`` () = let ``withHxReplaceUrl 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! _ = withHxRetarget "#somewhereElse" next ctx let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
Assert.True (dic.ContainsKey "HX-Retarget") Assert.True (dic.ContainsKey "HX-Replace-Url")
Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0]) Assert.Equal ("/a-substitute-url", dic["HX-Replace-Url"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTrigger succeeds`` () = let ``withHxNoReplaceUrl 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! _ = withHxTrigger "doSomething" next ctx let! _ = withHxNoReplaceUrl next ctx
Assert.True (dic.ContainsKey "HX-Trigger") Assert.True (dic.ContainsKey "HX-Replace-Url")
Assert.Equal ("doSomething", dic.["HX-Trigger"].[0]) Assert.Equal ("false", dic["HX-Replace-Url"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTriggerMany succeeds`` () = let ``withHxReswap 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! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Assert.True (dic.ContainsKey "HX-Trigger") Assert.True (dic.ContainsKey "HX-Reswap")
Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic.["HX-Trigger"].[0]) Assert.Equal (HxSwap.BeforeEnd, dic["HX-Reswap"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTriggerAfterSettle succeeds`` () = let ``withHxRetarget 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! _ = withHxTriggerAfterSettle "byTheWay" next ctx let! _ = withHxRetarget "#somewhereElse" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") Assert.True (dic.ContainsKey "HX-Retarget")
Assert.Equal ("byTheWay", dic.["HX-Trigger-After-Settle"].[0]) Assert.Equal ("#somewhereElse", dic["HX-Retarget"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTriggerManyAfterSettle succeeds`` () = let ``withHxTrigger 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! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx let! _ = withHxTrigger "doSomething" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic.["HX-Trigger-After-Settle"].[0]) Assert.Equal ("doSomething", dic["HX-Trigger"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTriggerAfterSwap succeeds`` () = let ``withHxTriggerMany 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! _ = withHxTriggerAfterSwap "justASec" next ctx let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("justASec", dic.["HX-Trigger-After-Swap"].[0]) Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic["HX-Trigger"][0])
} }
[<Fact>] [<Fact>]
let ``withHxTriggerManyAfterSwap succeeds`` () = let ``withHxTriggerAfterSettle 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! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap") Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("""{ "this": "1", "that": "2" }""", dic.["HX-Trigger-After-Swap"].[0]) Assert.Equal ("byTheWay", dic["HX-Trigger-After-Settle"][0])
} }
[<Fact>]
let ``withHxTriggerManyAfterSettle succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic["HX-Trigger-After-Settle"][0])
}
[<Fact>]
let ``withHxTriggerAfterSwap succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerAfterSwap "justASec" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
Assert.Equal ("justASec", dic["HX-Trigger-After-Swap"][0])
}
[<Fact>]
let ``withHxTriggerManyAfterSwap succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
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,102 +6,122 @@ 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
/// Indicates that the request is via an element using `hx-boost` /// Indicates that the request is via an element using `hx-boost`
member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse
/// The current URL of the browser _(note that this does not update until after settle)_ /// The current URL of the browser _(note that this does not update until after settle)_
member this.HxCurrentUrl with get () = hdr this "HX-Current-URL" |> Option.map Uri member this.HxCurrentUrl with get () = hdr this "HX-Current-URL" |> Option.map Uri
/// `true` if the request is for history restoration after a miss in the local history cache /// `true` if the request is for history restoration after a miss in the local history cache
member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
/// The user response to an `hx-prompt` /// The user response to an `hx-prompt`
member this.HxPrompt with get () = hdr this "HX-Prompt" member this.HxPrompt with get () = hdr this "HX-Prompt"
/// `true` if the request came from HTMX /// `true` if the request came from HTMX
member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse
/// The `id` of the target element if it exists /// The `id` of the target element if it exists
member this.HxTarget with get () = hdr this "HX-Target" member this.HxTarget with get () = hdr this "HX-Target"
/// The `id` of the triggered element if it exists /// The `id` of the triggered element if it exists
member this.HxTrigger with get () = hdr this "HX-Trigger" member this.HxTrigger with get () = hdr this "HX-Trigger"
/// The `name` of the triggered element if it exists /// The `name` of the triggered element if it exists
member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" member this.HxTriggerName with get () = hdr this "HX-Trigger-Name"
/// 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)
/// HTTP handlers for setting output headers /// HTTP handlers for setting output headers
[<AutoOpen>] [<AutoOpen>]
module Handlers = module Handlers =
/// Convert a boolean to lowercase `true` or `false` /// Convert a boolean to lowercase `true` or `false`
let private toLowerBool (trueOrFalse : bool) = let private toLowerBool (trueOrFalse : bool) =
(string trueOrFalse).ToLowerInvariant () (string trueOrFalse).ToLowerInvariant ()
/// 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 (evts : (string * string) list) = let private toJson (evts : (string * string) list) =
evts evts
|> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\""))) |> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\"")))
|> 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 /// Explicitly do not push a new URL into the history stack
let withHxNoPush : HttpHandler = let withHxNoPushUrl : HttpHandler =
toLowerBool false |> withHxPush toLowerBool false |> withHxPushUrl
/// Can be used to do a client-side redirect to a new location /// Pushes a new url into the history stack
let withHxRedirect : string -> HttpHandler = [<Obsolete "Use withHxPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
setHttpHeader "HX-Redirect" let withHxPush = withHxPushUrl
/// If set to `true` the client side will do a a full refresh of the page /// Explicitly do not push a new URL into the history stack
let withHxRefresh : bool -> HttpHandler = [<Obsolete "Use withHxNoPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
toLowerBool >> setHttpHeader "HX-Refresh" let withHxNoPush = withHxNoPushUrl
/// Allows you to override the `hx-target` attribute /// Can be used to do a client-side redirect to a new location
let withHxRetarget : string -> HttpHandler = let withHxRedirect : string -> HttpHandler =
setHttpHeader "HX-Retarget" setHttpHeader "HX-Redirect"
/// Allows you to trigger a single client side event /// If set to `true` the client side will do a a full refresh of the page
let withHxTrigger : string -> HttpHandler = let withHxRefresh : bool -> HttpHandler =
setHttpHeader "HX-Trigger" toLowerBool >> setHttpHeader "HX-Refresh"
/// Allows you to trigger multiple client side events /// Replaces the current URL in the history stack
let withHxTriggerMany evts : HttpHandler = let withHxReplaceUrl : string -> HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger" setHttpHeader "HX-Replace-Url"
/// Allows you to trigger a single client side event after changes have settled /// Explicitly do not replace the current URL in the history stack
let withHxTriggerAfterSettle : string -> HttpHandler = let withHxNoReplaceUrl : HttpHandler =
setHttpHeader "HX-Trigger-After-Settle" toLowerBool false |> withHxReplaceUrl
/// Allows you to trigger multiple client side events after changes have settled /// Override the `hx-swap` attribute from the initiating element
let withHxTriggerManyAfterSettle evts : HttpHandler = let withHxReswap : string -> HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle" setHttpHeader "HX-Reswap"
/// Allows you to trigger a single client side event after DOM swapping occurs /// Allows you to override the `hx-target` attribute
let withHxTriggerAfterSwap : string -> HttpHandler = let withHxRetarget : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Swap" setHttpHeader "HX-Retarget"
/// Allows you to trigger multiple client side events after DOM swapping occurs /// Allows you to trigger a single client side event
let withHxTriggerManyAfterSwap evts : HttpHandler = let withHxTrigger : string -> HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap" setHttpHeader "HX-Trigger"
/// Allows you to trigger multiple client side events
let withHxTriggerMany evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger"
/// Allows you to trigger a single client side event after changes have settled
let withHxTriggerAfterSettle : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Settle"
/// Allows you to trigger multiple client side events after changes have settled
let withHxTriggerManyAfterSettle evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
/// Allows you to trigger a single client side event after DOM swapping occurs
let withHxTriggerAfterSwap : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Swap"
/// Allows you to trigger multiple client side events after DOM swapping occurs
let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap"

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`. This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 1.7.0** **htmx version: 1.8.4**
### Setup ### Setup
@@ -27,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

@@ -6,462 +6,574 @@ open Xunit
/// Tests for the HxEncoding module /// Tests for the HxEncoding module
module Encoding = module Encoding =
[<Fact>] [<Fact>]
let ``Form is correct`` () = let ``Form is correct`` () =
Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form) Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form)
[<Fact>] [<Fact>]
let ``MultipartForm is correct`` () = let ``MultipartForm is correct`` () =
Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm) Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm)
/// Tests for the HxHeaders module /// Tests for the HxHeaders module
module Headers = module Headers =
[<Fact>] [<Fact>]
let ``From succeeds with an empty list`` () = let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxHeaders.From []) Assert.Equal ("{ }", HxHeaders.From [])
[<Fact>] [<Fact>]
let ``From succeeds and escapes quotes`` () = let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }", Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }",
HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ]) HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ])
/// Tests for the HxParams module /// Tests for the HxParams module
module Params = module Params =
[<Fact>] [<Fact>]
let ``All is correct`` () = let ``All is correct`` () =
Assert.Equal ("*", HxParams.All) Assert.Equal ("*", HxParams.All)
[<Fact>] [<Fact>]
let ``None is correct`` () = let ``None is correct`` () =
Assert.Equal ("none", HxParams.None) Assert.Equal ("none", HxParams.None)
[<Fact>] [<Fact>]
let ``With succeeds with empty list`` () = let ``With succeeds with empty list`` () =
Assert.Equal ("", HxParams.With []) Assert.Equal ("", HxParams.With [])
[<Fact>] [<Fact>]
let ``With succeeds with one list item`` () = let ``With succeeds with one list item`` () =
Assert.Equal ("boo", HxParams.With [ "boo" ]) Assert.Equal ("boo", HxParams.With [ "boo" ])
[<Fact>] [<Fact>]
let ``With succeeds with multiple list items`` () = let ``With succeeds with multiple list items`` () =
Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ]) Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ])
[<Fact>] [<Fact>]
let ``Except succeeds with empty list`` () = let ``Except succeeds with empty list`` () =
Assert.Equal ("not ", HxParams.Except []) Assert.Equal ("not ", HxParams.Except [])
[<Fact>] [<Fact>]
let ``Except succeeds with one list item`` () = let ``Except succeeds with one list item`` () =
Assert.Equal ("not that", HxParams.Except [ "that" ]) Assert.Equal ("not that", HxParams.Except [ "that" ])
[<Fact>] [<Fact>]
let ``Except succeeds with multiple list items`` () = let ``Except succeeds with multiple list items`` () =
Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ]) Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ])
/// Tests for the HxRequest module /// Tests for the HxRequest module
module Request = module Request =
[<Fact>] [<Fact>]
let ``Configure succeeds with an empty list`` () = let ``Configure succeeds with an empty list`` () =
Assert.Equal ("{ }", HxRequest.Configure []) Assert.Equal ("{ }", HxRequest.Configure [])
[<Fact>] [<Fact>]
let ``Configure succeeds with a non-empty list`` () = let ``Configure succeeds with a non-empty list`` () =
Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ])
[<Fact>] [<Fact>]
let ``Configure succeeds with all known params configured`` () = let ``Configure succeeds with all known params configured`` () =
Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }", Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }",
HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ]) HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
[<Fact>] [<Fact>]
let ``Timeout succeeds`` () = let ``Timeout succeeds`` () =
Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50) Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50)
[<Fact>] [<Fact>]
let ``Credentials succeeds when set to true`` () = let ``Credentials succeeds when set to true`` () =
Assert.Equal ("\"credentials\": true", HxRequest.Credentials true) Assert.Equal ("\"credentials\": true", HxRequest.Credentials true)
[<Fact>] [<Fact>]
let ``Credentials succeeds when set to false`` () = let ``Credentials succeeds when set to false`` () =
Assert.Equal ("\"credentials\": false", HxRequest.Credentials false) Assert.Equal ("\"credentials\": false", HxRequest.Credentials false)
[<Fact>] [<Fact>]
let ``NoHeaders succeeds when set to true`` () = let ``NoHeaders succeeds when set to true`` () =
Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true) Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true)
[<Fact>] [<Fact>]
let ``NoHeaders succeeds when set to false`` () = let ``NoHeaders succeeds when set to false`` () =
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 =
[<Fact>] [<Fact>]
let ``Click is correct`` () = let ``Click is correct`` () =
Assert.Equal ("click", HxTrigger.Click) Assert.Equal ("click", HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Load is correct`` () = let ``Load is correct`` () =
Assert.Equal ("load", HxTrigger.Load) Assert.Equal ("load", HxTrigger.Load)
[<Fact>] [<Fact>]
let ``Revealed is correct`` () = let ``Revealed is correct`` () =
Assert.Equal ("revealed", HxTrigger.Revealed) Assert.Equal ("revealed", HxTrigger.Revealed)
[<Fact>] [<Fact>]
let ``Every succeeds`` () = let ``Every succeeds`` () =
Assert.Equal ("every 3s", HxTrigger.Every "3s") Assert.Equal ("every 3s", HxTrigger.Every "3s")
[<Fact>] [<Fact>]
let ``Filter.Alt succeeds`` () = let ``Filter.Alt succeeds`` () =
Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click) Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.Ctrl succeeds`` () = let ``Filter.Ctrl succeeds`` () =
Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click) Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.Shift succeeds`` () = let ``Filter.Shift succeeds`` () =
Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click) Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.CtrlAlt succeeds`` () = let ``Filter.CtrlAlt succeeds`` () =
Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click) Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.CtrlShift succeeds`` () = let ``Filter.CtrlShift succeeds`` () =
Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click) Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.CtrlAltShift succeeds`` () = let ``Filter.CtrlAltShift succeeds`` () =
Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click) Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Filter.AltShift succeeds`` () = let ``Filter.AltShift succeeds`` () =
Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click) Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click)
[<Fact>] [<Fact>]
let ``Once succeeds when it is the first modifier`` () = let ``Once succeeds when it is the first modifier`` () =
Assert.Equal ("once", HxTrigger.Once "") Assert.Equal ("once", HxTrigger.Once "")
[<Fact>] [<Fact>]
let ``Once succeeds when it is not the first modifier`` () = let ``Once succeeds when it is not the first modifier`` () =
Assert.Equal ("click once", HxTrigger.Once "click") Assert.Equal ("click once", HxTrigger.Once "click")
[<Fact>] [<Fact>]
let ``Changed succeeds when it is the first modifier`` () = let ``Changed succeeds when it is the first modifier`` () =
Assert.Equal ("changed", HxTrigger.Changed "") Assert.Equal ("changed", HxTrigger.Changed "")
[<Fact>] [<Fact>]
let ``Changed succeeds when it is not the first modifier`` () = let ``Changed succeeds when it is not the first modifier`` () =
Assert.Equal ("click changed", HxTrigger.Changed "click") Assert.Equal ("click changed", HxTrigger.Changed "click")
[<Fact>] [<Fact>]
let ``Delay succeeds when it is the first modifier`` () = let ``Delay succeeds when it is the first modifier`` () =
Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "") Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "")
[<Fact>] [<Fact>]
let ``Delay succeeds when it is not the first modifier`` () = let ``Delay succeeds when it is not the first modifier`` () =
Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click") Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click")
[<Fact>] [<Fact>]
let ``Throttle succeeds when it is the first modifier`` () = let ``Throttle succeeds when it is the first modifier`` () =
Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "") Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "")
[<Fact>] [<Fact>]
let ``Throttle succeeds when it is not the first modifier`` () = let ``Throttle succeeds when it is not the first modifier`` () =
Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click") Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click")
[<Fact>] [<Fact>]
let ``From succeeds when it is the first modifier`` () = let ``From succeeds when it is the first modifier`` () =
Assert.Equal ("from:.nav", HxTrigger.From ".nav" "") Assert.Equal ("from:.nav", HxTrigger.From ".nav" "")
[<Fact>] [<Fact>]
let ``From succeeds when it is not the first modifier`` () = let ``From succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click") Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click")
[<Fact>] [<Fact>]
let ``FromDocument succeeds when it is the first modifier`` () = let ``FromDocument succeeds when it is the first modifier`` () =
Assert.Equal ("from:document", HxTrigger.FromDocument "") Assert.Equal ("from:document", HxTrigger.FromDocument "")
[<Fact>] [<Fact>]
let ``FromDocument succeeds when it is not the first modifier`` () = let ``FromDocument succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:document", HxTrigger.FromDocument "click") Assert.Equal ("click from:document", HxTrigger.FromDocument "click")
[<Fact>] [<Fact>]
let ``FromWindow succeeds when it is the first modifier`` () = let ``FromWindow succeeds when it is the first modifier`` () =
Assert.Equal ("from:window", HxTrigger.FromWindow "") Assert.Equal ("from:window", HxTrigger.FromWindow "")
[<Fact>] [<Fact>]
let ``FromWindow succeeds when it is not the first modifier`` () = let ``FromWindow succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:window", HxTrigger.FromWindow "click") Assert.Equal ("click from:window", HxTrigger.FromWindow "click")
[<Fact>] [<Fact>]
let ``FromClosest succeeds when it is the first modifier`` () = let ``FromClosest succeeds when it is the first modifier`` () =
Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "") Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "")
[<Fact>] [<Fact>]
let ``FromClosest succeeds when it is not the first modifier`` () = let ``FromClosest succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click") Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click")
[<Fact>] [<Fact>]
let ``FromFind succeeds when it is the first modifier`` () = let ``FromFind succeeds when it is the first modifier`` () =
Assert.Equal ("from:find li", HxTrigger.FromFind "li" "") Assert.Equal ("from:find li", HxTrigger.FromFind "li" "")
[<Fact>] [<Fact>]
let ``FromFind succeeds when it is not the first modifier`` () = let ``FromFind succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click") Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click")
[<Fact>] [<Fact>]
let ``Target succeeds when it is the first modifier`` () = let ``Target succeeds when it is the first modifier`` () =
Assert.Equal ("target:main", HxTrigger.Target "main" "") Assert.Equal ("target:main", HxTrigger.Target "main" "")
[<Fact>] [<Fact>]
let ``Target succeeds when it is not the first modifier`` () = let ``Target succeeds when it is not the first modifier`` () =
Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click") Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click")
[<Fact>] [<Fact>]
let ``Consume succeeds when it is the first modifier`` () = let ``Consume succeeds when it is the first modifier`` () =
Assert.Equal ("consume", HxTrigger.Consume "") Assert.Equal ("consume", HxTrigger.Consume "")
[<Fact>] [<Fact>]
let ``Consume succeeds when it is not the first modifier`` () = let ``Consume succeeds when it is not the first modifier`` () =
Assert.Equal ("click consume", HxTrigger.Consume "click") Assert.Equal ("click consume", HxTrigger.Consume "click")
[<Fact>] [<Fact>]
let ``Queue succeeds when it is the first modifier`` () = let ``Queue succeeds when it is the first modifier`` () =
Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "") Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "")
[<Fact>] [<Fact>]
let ``Queue succeeds when it is not the first modifier`` () = let ``Queue succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click") Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click")
[<Fact>] [<Fact>]
let ``QueueFirst succeeds when it is the first modifier`` () = let ``QueueFirst succeeds when it is the first modifier`` () =
Assert.Equal ("queue:first", HxTrigger.QueueFirst "") Assert.Equal ("queue:first", HxTrigger.QueueFirst "")
[<Fact>] [<Fact>]
let ``QueueFirst succeeds when it is not the first modifier`` () = let ``QueueFirst succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click") Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click")
[<Fact>] [<Fact>]
let ``QueueLast succeeds when it is the first modifier`` () = let ``QueueLast succeeds when it is the first modifier`` () =
Assert.Equal ("queue:last", HxTrigger.QueueLast "") Assert.Equal ("queue:last", HxTrigger.QueueLast "")
[<Fact>] [<Fact>]
let ``QueueLast succeeds when it is not the first modifier`` () = let ``QueueLast succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:last", HxTrigger.QueueLast "click") Assert.Equal ("click queue:last", HxTrigger.QueueLast "click")
[<Fact>] [<Fact>]
let ``QueueAll succeeds when it is the first modifier`` () = let ``QueueAll succeeds when it is the first modifier`` () =
Assert.Equal ("queue:all", HxTrigger.QueueAll "") Assert.Equal ("queue:all", HxTrigger.QueueAll "")
[<Fact>] [<Fact>]
let ``QueueAll succeeds when it is not the first modifier`` () = let ``QueueAll succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:all", HxTrigger.QueueAll "click") Assert.Equal ("click queue:all", HxTrigger.QueueAll "click")
[<Fact>] [<Fact>]
let ``QueueNone succeeds when it is the first modifier`` () = let ``QueueNone succeeds when it is the first modifier`` () =
Assert.Equal ("queue:none", HxTrigger.QueueNone "") Assert.Equal ("queue:none", HxTrigger.QueueNone "")
[<Fact>] [<Fact>]
let ``QueueNone succeeds when it is not the first modifier`` () = let ``QueueNone succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:none", HxTrigger.QueueNone "click") Assert.Equal ("click queue:none", HxTrigger.QueueNone "click")
/// Tests for the HxVals module /// Tests for the HxVals module
module Vals = module Vals =
[<Fact>] [<Fact>]
let ``From succeeds with an empty list`` () = let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxVals.From []) Assert.Equal ("{ }", HxVals.From [])
[<Fact>] [<Fact>]
let ``From succeeds and escapes quotes`` () = let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }", Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }",
HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ]) HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ])
/// Tests for the HtmxAttrs module /// Tests for the HtmxAttrs module
module Attributes = module Attributes =
/// Pipe-able assertion for a rendered node /// Pipe-able assertion for a rendered node
let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node) let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node)
[<Fact>] [<Fact>]
let ``_hxBoost succeeds`` () = let ``_hxBoost succeeds`` () =
div [ _hxBoost ] [] |> shouldRender """<div hx-boost="true"></div>""" div [ _hxBoost ] [] |> shouldRender """<div hx-boost="true"></div>"""
[<Fact>] [<Fact>]
let ``_hxConfirm succeeds`` () = let ``_hxConfirm succeeds`` () =
button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """<button hx-confirm="REALLY?!?"></button>""" button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """<button hx-confirm="REALLY?!?"></button>"""
[<Fact>] [<Fact>]
let ``_hxDelete succeeds`` () = let ``_hxDelete succeeds`` () =
span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """<span hx-delete="/this-endpoint"></span>""" span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """<span hx-delete="/this-endpoint"></span>"""
[<Fact>] [<Fact>]
let ``_hxDisable succeeds`` () = let ``_hxDisable succeeds`` () =
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>""" p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
[<Fact>] [<Fact>]
let ``_hxDisinherit succeeds`` () = let ``_hxDisinherit succeeds`` () =
strong [ _hxDisinherit "*" ] [] |> shouldRender """<strong hx-disinherit="*"></strong>""" 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>"""
[<Fact>] [<Fact>]
let ``_hxExt succeeds`` () = let ``_hxExt succeeds`` () =
section [ _hxExt "extendme" ] [] |> shouldRender """<section hx-ext="extendme"></section>""" section [ _hxExt "extendme" ] [] |> shouldRender """<section hx-ext="extendme"></section>"""
[<Fact>] [<Fact>]
let ``_hxGet succeeds`` () = let ``_hxGet succeeds`` () =
article [ _hxGet "/the-text" ] [] |> shouldRender """<article hx-get="/the-text"></article>""" article [ _hxGet "/the-text" ] [] |> shouldRender """<article hx-get="/the-text"></article>"""
[<Fact>] [<Fact>]
let ``_hxHeaders succeeds`` () = let ``_hxHeaders succeeds`` () =
figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] []
|> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>""" |> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>"""
[<Fact>] [<Fact>]
let ``_hxHistoryElt succeeds`` () = let ``_hxHistoryElt succeeds`` () =
table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>""" table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>"""
[<Fact>] [<Fact>]
let ``_hxInclude succeeds`` () = let ``_hxInclude succeeds`` () =
a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """<a hx-include=".extra-stuff"></a>""" a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """<a hx-include=".extra-stuff"></a>"""
[<Fact>] [<Fact>]
let ``_hxIndicator succeeds`` () = let ``_hxIndicator succeeds`` () =
aside [ _hxIndicator "#spinner" ] [] |> shouldRender """<aside hx-indicator="#spinner"></aside>""" aside [ _hxIndicator "#spinner" ] [] |> shouldRender """<aside hx-indicator="#spinner"></aside>"""
[<Fact>] [<Fact>]
let ``_hxNoBoost succeeds`` () = let ``_hxNoBoost succeeds`` () =
td [ _hxNoBoost ] [] |> shouldRender """<td hx-boost="false"></td>""" td [ _hxNoBoost ] [] |> shouldRender """<td hx-boost="false"></td>"""
[<Fact>] [<Fact>]
let ``_hxParams succeeds`` () = let ``_hxParams succeeds`` () =
br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">""" br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">"""
[<Fact>] [<Fact>]
let ``_hxPatch succeeds`` () = let ``_hxPatch succeeds`` () =
div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>""" div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>"""
[<Fact>] [<Fact>]
let ``_hxPost succeeds`` () = let ``_hxPost succeeds`` () =
hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">""" hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">"""
[<Fact>] [<Fact>]
let ``_hxPreserve succeeds`` () = let ``_hxPreserve succeeds`` () =
img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">""" img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">"""
[<Fact>] [<Fact>]
let ``_hxPrompt succeeds`` () = let ``_hxPrompt succeeds`` () =
strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """<strong hx-prompt="Who goes there?"></strong>""" strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """<strong hx-prompt="Who goes there?"></strong>"""
[<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>] [<Fact>]
let ``_hxRequest succeeds`` () = let ``_hxReplaceUrl succeeds`` () =
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>""" p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """<p hx-replace-url="/something-else"></p>"""
[<Fact>] [<Fact>]
let ``_hxSelect succeeds`` () = let ``_hxRequest succeeds`` () =
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>""" u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
[<Fact>] [<Fact>]
let ``_hxSse succeeds`` () = let ``_hxSelect succeeds`` () =
footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """<footer hx-sse="connect:/my-events"></footer>""" nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
[<Fact>] [<Fact>]
let ``_hxSwap succeeds`` () = let ``_hxSelectOob succeeds`` () =
del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>""" section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
[<Fact>] [<Fact>]
let ``_hxSwapOob succeeds`` () = let ``_hxSse succeeds`` () =
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>""" footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """<footer hx-sse="connect:/my-events"></footer>"""
[<Fact>] [<Fact>]
let ``_hxSync succeeds`` () = let ``_hxSwap succeeds`` () =
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>""" del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>"""
[<Fact>] [<Fact>]
let ``_hxTarget succeeds`` () = let ``_hxSwapOob succeeds`` () =
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>""" li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
[<Fact>] [<Fact>]
let ``_hxTrigger succeeds`` () = let ``_hxSync succeeds`` () =
figcaption [ _hxTrigger "load" ] [] |> shouldRender """<figcaption hx-trigger="load"></figcaption>""" nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
[<Fact>] [<Fact>]
let ``_hxVals succeeds`` () = let ``_hxTarget succeeds`` () =
dt [ _hxVals """{ "extra": "values" }""" ] [] header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
|> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>"""
[<Fact>] [<Fact>]
let ``_hxWs succeeds`` () = let ``_hxTrigger succeeds`` () =
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>""" figcaption [ _hxTrigger "load" ] [] |> shouldRender """<figcaption hx-trigger="load"></figcaption>"""
[<Fact>]
let ``_hxVals succeeds`` () =
dt [ _hxVals """{ "extra": "values" }""" ] []
|> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>"""
[<Fact>]
let ``_hxWs succeeds`` () =
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""
/// Tests for the Script module /// Tests for the Script module
module Script = module Script =
[<Fact>] [<Fact>]
let ``Script.minified succeeds`` () = let ``Script.minified succeeds`` () =
let html = RenderView.AsString.htmlNode Script.minified let html = RenderView.AsString.htmlNode Script.minified
Assert.Equal ("""<script src="https://unpkg.com/htmx.org@1.7.0" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script>""", Assert.Equal
html) ("""<script src="https://unpkg.com/htmx.org@1.8.4" integrity="sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" crossorigin="anonymous"></script>""",
html)
[<Fact>] [<Fact>]
let ``Script.unminified succeeds`` () = let ``Script.unminified succeeds`` () =
let html = RenderView.AsString.htmlNode Script.unminified let html = RenderView.AsString.htmlNode Script.unminified
Assert.Equal ("""<script src="https://unpkg.com/htmx.org@1.7.0/dist/htmx.js" integrity="sha384-ESk4PjE7dwjGkEciohREmmf8rLMX0E95MKwxM3bvC90sZ3XbF2TELnVk2w7bX0d9" crossorigin="anonymous"></script>""", Assert.Equal
html) ("""<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,238 +1,372 @@
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) =
kvps kvps
|> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\""))) |> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\"")))
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
/// Valid values for the `hx-encoding` attribute /// Valid values for the `hx-encoding` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxEncoding = module HxEncoding =
/// A standard HTTP form
let Form = "application/x-www-form-urlencoded" /// A standard HTTP form
/// A multipart form (used for file uploads) let Form = "application/x-www-form-urlencoded"
let MultipartForm = "multipart/form-data"
/// A multipart form (used for file uploads)
let MultipartForm = "multipart/form-data"
/// 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
let From = toJson /// Create headers from a list of key/value pairs
let From = toJson
/// Values / helpers for the `hx-params` attribute /// Values / helpers for the `hx-params` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxParams = module HxParams =
/// Include all parameters
let All = "*" /// Include all parameters
/// Include no parameters let All = "*"
let None = "none"
/// Include the specified parameters /// Include no parameters
let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") let None = "none"
/// Exclude the specified parameters
let Except fields = With fields |> sprintf "not %s" /// Include the specified parameters
let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
/// Exclude the specified parameters
let Except fields = With fields |> sprintf "not %s"
/// 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
let private toLowerBool (it : bool) =
(string it).ToLowerInvariant ()
/// Configure the request with various options
let Configure (opts : string list) =
opts
|> String.concat ", "
|> sprintf "{ %s }"
/// Set a timeout (in milliseconds)
let Timeout (ms : int) = $"\"timeout\": {ms}"
/// Include or exclude credentials from the request
let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
/// Exclude or include headers from the request
let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
/// Convert a boolean to its lowercase string equivalent
let private toLowerBool (it : bool) =
(string it).ToLowerInvariant ()
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config) /// Configure the request with various options
[<RequireQualifiedAccess>] let Configure (opts : string list) =
module HxSwap = opts
/// The default, replace the inner html of the target element |> String.concat ", "
let InnerHtml = "innerHTML" |> sprintf "{ %s }"
/// Replace the entire target element with the response
let OuterHtml = "outerHTML" /// Set a timeout (in milliseconds)
/// Insert the response before the target element let Timeout (ms : int) = $"\"timeout\": {ms}"
let BeforeBegin = "beforebegin"
/// Insert the response before the first child of the target element /// Include or exclude credentials from the request
let AfterBegin = "afterbegin" let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
/// Insert the response after the last child of the target element
let BeforeEnd = "beforeend" /// Exclude or include headers from the request
/// Insert the response after the target element let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
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
let private appendFilter filter (trigger : string) = /// Append a filter to a trigger
match trigger.Contains "[" with let private appendFilter filter (trigger : string) =
| true -> match trigger.Contains "[" with
let parts = trigger.Split ('[', ']') | true ->
$"{parts.[0]}[{parts.[1]}&&{filter}]" let parts = trigger.Split ('[', ']')
| false -> $"{trigger}[{filter}]" $"{parts[0]}[{parts[1]}&&{filter}]"
/// Trigger the event on a click | false -> $"{trigger}[{filter}]"
let Click = "click"
/// Trigger the event on page load /// Trigger the event on a click
let Load = "load" let Click = "click"
/// Trigger the event when the item is visible
let Revealed = "revealed" /// Trigger the event on page load
/// Trigger this event every [timing declaration] let Load = "load"
let Every (duration : string) = $"every {duration}"
/// Helpers for defining filters /// Trigger the event when the item is visible
module Filter = let Revealed = "revealed"
/// Only trigger the event if the `ALT` key is pressed
let Alt = appendFilter "altKey" /// Trigger this event every [timing declaration]
/// Only trigger the event if the `CTRL` key is pressed let Every (duration : string) = $"every {duration}"
let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if the `SHIFT` key is pressed /// Helpers for defining filters
let Shift = appendFilter "shiftKey" module Filter =
/// Only trigger the event if `CTRL+ALT` are pressed
let CtrlAlt = Ctrl >> Alt /// Only trigger the event if the `ALT` key is pressed
/// Only trigger the event if `CTRL+SHIFT` are pressed let Alt = appendFilter "altKey"
let CtrlShift = Ctrl >> Shift
/// Only trigger the event if `CTRL+ALT+SHIFT` are pressed /// Only trigger the event if the `CTRL` key is pressed
let CtrlAltShift = CtrlAlt >> Shift let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if `ALT+SHIFT` are pressed
let AltShift = Alt >> Shift /// Only trigger the event if the `SHIFT` key is pressed
/// Append a modifier to the current trigger let Shift = appendFilter "shiftKey"
let private appendModifier modifier current =
match current with "" -> modifier | _ -> $"{current} {modifier}" /// Only trigger the event if `CTRL+ALT` are pressed
/// Only trigger once let CtrlAlt = Ctrl >> Alt
let Once = appendModifier "once"
/// Trigger when changed /// Only trigger the event if `CTRL+SHIFT` are pressed
let Changed = appendModifier "changed" let CtrlShift = Ctrl >> Shift
/// Delay execution; resets every time the event is seen
let Delay = sprintf "delay:%s" >> appendModifier /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed
/// Throttle execution; ignore other events, fire when duration passes let CtrlAltShift = CtrlAlt >> Shift
let Throttle = sprintf "throttle:%s" >> appendModifier
/// Trigger this event from a CSS selector /// Only trigger the event if `ALT+SHIFT` are pressed
let From = sprintf "from:%s" >> appendModifier let AltShift = Alt >> Shift
/// Trigger this event from the `document` object
let FromDocument = From "document" /// Append a modifier to the current trigger
/// Trigger this event from the `window` object let private appendModifier modifier current =
let FromWindow = From "window" if current = "" then modifier else $"{current} {modifier}"
/// Trigger this event from the closest parent CSS selector
let FromClosest = sprintf "closest %s" >> From /// Only trigger once
/// Trigger this event from the closest child CSS selector let Once = appendModifier "once"
let FromFind = sprintf "find %s" >> From
/// Target the given CSS selector with the results of this event /// Trigger when changed
let Target = sprintf "target:%s" >> appendModifier let Changed = appendModifier "changed"
/// Prevent any further events from occurring after this one fires
let Consume = appendModifier "consume" /// Delay execution; resets every time the event is seen
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" let Delay = sprintf "delay:%s" >> appendModifier
let Queue = sprintf "queue:%s" >> appendModifier
/// Queue the first event, discard all others (i.e., a FIFO queue of 1) /// Throttle execution; ignore other events, fire when duration passes
let QueueFirst = Queue "first" let Throttle = sprintf "throttle:%s" >> appendModifier
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
let QueueLast = Queue "last" /// Trigger this event from a CSS selector
/// Queue all events; discard none let From = sprintf "from:%s" >> appendModifier
let QueueAll = Queue "all"
/// Queue no events; discard all /// Trigger this event from the `document` object
let QueueNone = Queue "none" let FromDocument = From "document"
/// Trigger this event from the `window` object
let FromWindow = From "window"
/// Trigger this event from the closest parent CSS selector
let FromClosest = sprintf "closest %s" >> From
/// Trigger this event from the closest child CSS selector
let FromFind = sprintf "find %s" >> From
/// Target the given CSS selector with the results of this event
let Target = sprintf "target:%s" >> appendModifier
/// Prevent any further events from occurring after this one fires
let Consume = appendModifier "consume"
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last"
let Queue = sprintf "queue:%s" >> appendModifier
/// Queue the first event, discard all others (i.e., a FIFO queue of 1)
let QueueFirst = Queue "first"
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
let QueueLast = Queue "last"
/// Queue all events; discard none
let QueueAll = Queue "all"
/// Queue no events; discard all
let QueueNone = Queue "none"
/// 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
let From = toJson /// Create values from a list of key/value pairs
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)
let _hxBoost = attr "hx-boost" "true" /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
/// Shows a confirm() dialog before issuing a request let _hxBoost = attr "hx-boost" "true"
let _hxConfirm = attr "hx-confirm"
/// Issues a DELETE to the specified URL /// Shows a confirm() dialog before issuing a request
let _hxDelete = attr "hx-delete" let _hxConfirm = attr "hx-confirm"
/// Disables htmx processing for the given node and any children nodes
let _hxDisable = flag "hx-disable" /// Issues a DELETE to the specified URL
/// Disinherit all ("*") or specific htmx attributes let _hxDelete = attr "hx-delete"
let _hxDisinherit = attr "hx-disinherit"
/// Changes the request encoding type /// Disables htmx processing for the given node and any children nodes
let _hxEncoding = attr "hx-encoding" let _hxDisable = flag "hx-disable"
/// Extensions to use for this element
let _hxExt = attr "hx-ext" /// Disinherit all ("*") or specific htmx attributes
/// Issues a GET to the specified URL let _hxDisinherit = attr "hx-disinherit"
let _hxGet = attr "hx-get"
/// Adds to the headers that will be submitted with the request /// Changes the request encoding type
let _hxHeaders = attr "hx-headers" let _hxEncoding = attr "hx-encoding"
/// The element to snapshot and restore during history navigation
let _hxHistoryElt = flag "hx-history-elt" /// Extensions to use for this element
/// Includes additional data in AJAX requests let _hxExt = attr "hx-ext"
let _hxInclude = attr "hx-include"
/// The element to put the htmx-request class on during the AJAX request /// Issues a GET to the specified URL
let _hxIndicator = attr "hx-indicator" let _hxGet = attr "hx-get"
/// Overrides a previous `hx-boost`
let _hxNoBoost = attr "hx-boost" "false" /// Adds to the headers that will be submitted with the request
/// Filters the parameters that will be submitted with a request let _hxHeaders = attr "hx-headers"
let _hxParams = attr "hx-params"
/// Issues a PATCH to the specified URL /// The element to snapshot and restore during history navigation
let _hxPatch = attr "hx-patch" let _hxHistoryElt = flag "hx-history-elt"
/// Issues a POST to the specified URL
let _hxPost = attr "hx-post" /// Includes additional data in AJAX requests
/// Preserves an element between requests let _hxInclude = attr "hx-include"
let _hxPreserve = attr "hx-preserve" "true"
/// Shows a prompt before submitting a request /// The element to put the htmx-request class on during the AJAX request
let _hxPrompt = attr "hx-prompt" let _hxIndicator = attr "hx-indicator"
/// Pushes the URL into the location bar, creating a new history entry
let _hxPushUrl = flag "hx-push-url" /// Overrides a previous `hx-boost`
/// Issues a PUT to the specified URL let _hxNoBoost = attr "hx-boost" "false"
let _hxPut = attr "hx-put"
/// Configures various aspects of the request /// Filters the parameters that will be submitted with a request
let _hxRequest = attr "hx-request" let _hxParams = attr "hx-params"
/// Selects a subset of the server response to process
let _hxSelect = attr "hx-select" /// Issues a PATCH to the specified URL
/// Establishes and listens to Server Sent Event (SSE) sources for events let _hxPatch = attr "hx-patch"
let _hxSse = attr "hx-sse"
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd') /// Issues a POST to the specified URL
let _hxSwap = attr "hx-swap" let _hxPost = attr "hx-post"
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
let _hxSwapOob = attr "hx-swap-oob" /// Preserves an element between requests
/// Synchronize events based on another element let _hxPreserve = attr "hx-preserve" "true"
let _hxSync = attr "hx-sync"
/// Specifies the target element to be swapped /// Shows a prompt before submitting a request
let _hxTarget = attr "hx-target" let _hxPrompt = attr "hx-prompt"
/// Specifies the event that triggers the request
let _hxTrigger = attr "hx-trigger" /// Pushes the URL into the location bar, creating a new history entry
/// Adds to the parameters that will be submitted with the request let _hxPushUrl = attr "hx-push-url"
let _hxVals = attr "hx-vals"
/// Establishes a WebSocket or sends information to one /// Issues a PUT to the specified URL
let _hxWs = attr "hx-ws" 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
let _hxRequest = attr "hx-request"
/// Selects a subset of the server response to process
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
let _hxSse = attr "hx-sse"
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd')
let _hxSwap = attr "hx-swap"
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
let _hxSwapOob = attr "hx-swap-oob"
/// Synchronize events based on another element
let _hxSync = attr "hx-sync"
/// Specifies the target element to be swapped
let _hxTarget = attr "hx-target"
/// Specifies the event that triggers the request
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
let _hxVals = attr "hx-vals"
/// Establishes a WebSocket or sends information to one
let _hxWs = attr "hx-ws"
/// Script tags to pull htmx into an web page /// Script tags to pull htmx into an web page
module Script = module Script =
/// Script tag to load the minified version from unpkg.com /// Script tag to load the minified version from unpkg.com
let minified = let minified =
script [ script [ _src "https://unpkg.com/htmx.org@1.8.4"
_src "https://unpkg.com/htmx.org@1.7.0" _integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
_integrity "sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" _crossorigin "anonymous" ] []
_crossorigin "anonymous"
] []
/// Script tag to load the unminified version from unpkg.com /// Script tag to load the unminified version from unpkg.com
let unminified = let unminified =
script [ script [ _src "https://unpkg.com/htmx.org@1.8.4/dist/htmx.js"
_src "https://unpkg.com/htmx.org@1.7.0/dist/htmx.js" _integrity "sha384-sh63gh7zpjxu153RyKJ06Oy5HxIVl6cchze/dJOHulOI7u0sGZoC/CfQJHPODhFn"
_integrity "sha384-ESk4PjE7dwjGkEciohREmmf8rLMX0E95MKwxM3bvC90sZ3XbF2TELnVk2w7bX0d9" _crossorigin "anonymous" ] []
_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,7 +2,7 @@
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.7.0** **htmx version: 1.8.4**
### Setup ### Setup
@@ -23,12 +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). 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: