diff --git a/src/Common.Tests/Giraffe.Htmx.Common.Tests.fsproj b/src/Common.Tests/Giraffe.Htmx.Common.Tests.fsproj new file mode 100644 index 0000000..58aa294 --- /dev/null +++ b/src/Common.Tests/Giraffe.Htmx.Common.Tests.fsproj @@ -0,0 +1,30 @@ + + + + false + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/src/Common.Tests/Program.fs b/src/Common.Tests/Program.fs new file mode 100644 index 0000000..fdc31cd --- /dev/null +++ b/src/Common.Tests/Program.fs @@ -0,0 +1 @@ +module Program = let [] main _ = 0 diff --git a/src/Common.Tests/Tests.fs b/src/Common.Tests/Tests.fs new file mode 100644 index 0000000..4172698 --- /dev/null +++ b/src/Common.Tests/Tests.fs @@ -0,0 +1,35 @@ +module Tests + +open Giraffe.Htmx +open Xunit + +/// Tests for the HxSwap module +module Swap = + + [] + let ``InnerHtml is correct`` () = + Assert.Equal ("innerHTML", HxSwap.InnerHtml) + + [] + let ``OuterHtml is correct`` () = + Assert.Equal ("outerHTML", HxSwap.OuterHtml) + + [] + let ``BeforeBegin is correct`` () = + Assert.Equal ("beforebegin", HxSwap.BeforeBegin) + + [] + let ``BeforeEnd is correct`` () = + Assert.Equal ("beforeend", HxSwap.BeforeEnd) + + [] + let ``AfterBegin is correct`` () = + Assert.Equal ("afterbegin", HxSwap.AfterBegin) + + [] + let ``AfterEnd is correct`` () = + Assert.Equal ("afterend", HxSwap.AfterEnd) + + [] + let ``None is correct`` () = + Assert.Equal ("none", HxSwap.None) diff --git a/src/Common/Common.fs b/src/Common/Common.fs new file mode 100644 index 0000000..9a75b8c --- /dev/null +++ b/src/Common/Common.fs @@ -0,0 +1,28 @@ +/// Common definitions shared between attribute values and response headers +[] +module Giraffe.Htmx.Common + +/// Valid values for the `hx-swap` attribute / `HX-Reswap` header (may be combined with swap/settle/scroll/show config) +[] +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" diff --git a/src/Common/Giraffe.Htmx.Common.fsproj b/src/Common/Giraffe.Htmx.Common.fsproj new file mode 100644 index 0000000..112a122 --- /dev/null +++ b/src/Common/Giraffe.Htmx.Common.fsproj @@ -0,0 +1,11 @@ + + + + true + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 560d16f..dba8c92 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,8 +1,9 @@  - 1.7.0 - Support new attributes/headers in htmx 1.7.0 + net6.0 + 1.8.0 + Support new attributes/headers in htmx 1.8.0 danieljsummers Bit Badger Solutions https://github.com/bit-badger/Giraffe.Htmx diff --git a/src/Giraffe.Htmx.sln b/src/Giraffe.Htmx.sln new file mode 100644 index 0000000..c1ebafb --- /dev/null +++ b/src/Giraffe.Htmx.sln @@ -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 diff --git a/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj index a9d2f4e..056f57c 100644 --- a/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj +++ b/src/Htmx.Tests/Giraffe.Htmx.Tests.fsproj @@ -1,8 +1,6 @@ - net6.0 - false false diff --git a/src/Htmx.Tests/Tests.fs b/src/Htmx.Tests/Tests.fs index 80fb933..213ac70 100644 --- a/src/Htmx.Tests/Tests.fs +++ b/src/Htmx.Tests/Tests.fs @@ -9,332 +9,364 @@ open Xunit /// Tests for the IHeaderDictionary extension properties module IHeaderDictionaryExtensions = - [] - let ``HxBoosted succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True + [] + let ``HxBoosted succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True - [] - let ``HxBoosted succeeds when the header is present and true`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Boosted", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True - Option.get ctx.Request.Headers.HxBoosted |> Assert.True + [] + let ``HxBoosted succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Boosted", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True + Option.get ctx.Request.Headers.HxBoosted |> Assert.True - [] - let ``HxBoosted succeeds when the header is present and false`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Boosted", "false") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True - Option.get ctx.Request.Headers.HxBoosted |> Assert.False + [] + let ``HxBoosted succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Boosted", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True + Option.get ctx.Request.Headers.HxBoosted |> Assert.False - [] - let ``HxCurrentUrl succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True + [] + let ``HxCurrentUrl succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True - [] - let ``HxCurrentUrl succeeds when the header is present`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Current-URL", "http://localhost/test.htm") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True - Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl) + [] + let ``HxCurrentUrl succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Current-URL", "http://localhost/test.htm") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True + Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl) - [] - let ``HxHistoryRestoreRequest succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + [] + let ``HxHistoryRestoreRequest succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True - [] - let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-History-Restore-Request", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True - Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + [] + let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-History-Restore-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True - [] - let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-History-Restore-Request", "false") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True - Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False + [] + let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-History-Restore-Request", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True + Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False - [] - let ``HxPrompt succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True + [] + let ``HxPrompt succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True - [] - let ``HxPrompt succeeds when the header is present`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Prompt", "of course") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True - Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt) + [] + let ``HxPrompt succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Prompt", "of course") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True + Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt) - [] - let ``HxRequest succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxRequest |> Assert.True + [] + let ``HxRequest succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxRequest |> Assert.True - [] - let ``HxRequest succeeds when the header is present and true`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxRequest |> Assert.True - Option.get ctx.Request.Headers.HxRequest |> Assert.True + [] + let ``HxRequest succeeds when the header is present and true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxRequest |> Assert.True + Option.get ctx.Request.Headers.HxRequest |> Assert.True - [] - let ``HxRequest succeeds when the header is present and false`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "false") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxRequest |> Assert.True - Option.get ctx.Request.Headers.HxRequest |> Assert.False + [] + let ``HxRequest succeeds when the header is present and false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "false") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxRequest |> Assert.True + Option.get ctx.Request.Headers.HxRequest |> Assert.False - [] - let ``HxTarget succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxTarget |> Assert.True + [] + let ``HxTarget succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTarget |> Assert.True - [] - let ``HxTarget succeeds when the header is present`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Target", "#leItem") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxTarget |> Assert.True - Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget) + [] + let ``HxTarget succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Target", "#leItem") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxTarget |> Assert.True + Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget) - [] - let ``HxTrigger succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True + [] + let ``HxTrigger succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True - [] - let ``HxTrigger succeeds when the header is present`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Trigger", "#trig") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True - Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger) + [] + let ``HxTrigger succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Trigger", "#trig") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True + Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger) - [] - let ``HxTriggerName succeeds when the header is not present`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True + [] + let ``HxTriggerName succeeds when the header is not present`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True - [] - let ``HxTriggerName succeeds when the header is present`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Trigger-Name", "click") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True - Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName) + [] + let ``HxTriggerName succeeds when the header is present`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Trigger-Name", "click") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True + Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName) /// Tests for the HttpRequest extension properties module HttpRequestExtensions = - [] - let ``IsHtmx succeeds when request is not from htmx`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Assert.False ctx.Request.IsHtmx + [] + let ``IsHtmx succeeds when request is not from htmx`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Assert.False ctx.Request.IsHtmx - [] - let ``IsHtmx succeeds when request is from htmx`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Assert.True ctx.Request.IsHtmx + [] + let ``IsHtmx succeeds when request is from htmx`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.True ctx.Request.IsHtmx - [] - let ``IsHtmxRefresh succeeds when request is not from htmx`` () = - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore - Assert.False ctx.Request.IsHtmxRefresh + [] + let ``IsHtmxRefresh succeeds when request is not from htmx`` () = + let ctx = Substitute.For () + ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + Assert.False ctx.Request.IsHtmxRefresh - [] - let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Assert.False ctx.Request.IsHtmxRefresh + [] + let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.False ctx.Request.IsHtmxRefresh - [] - let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") - dic.Add ("HX-History-Restore-Request", "true") - ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore - Assert.True ctx.Request.IsHtmxRefresh + [] + let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + dic.Add ("HX-Request", "true") + dic.Add ("HX-History-Restore-Request", "true") + ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore + Assert.True ctx.Request.IsHtmxRefresh /// Tests for the HttpHandler functions provided in the Handlers module module HandlerTests = - open System.Threading.Tasks - - /// Dummy "next" parameter to get the pipeline to execute/terminate - let next (ctx : HttpContext) = Task.FromResult (Some ctx) - - [] - let ``withHxPush succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxPush "/a-new-url" next ctx - Assert.True (dic.ContainsKey "HX-Push") - Assert.Equal ("/a-new-url", dic.["HX-Push"].[0]) - } - - [] - let ``withHxNoPush succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxNoPush next ctx - Assert.True (dic.ContainsKey "HX-Push") - Assert.Equal ("false", dic.["HX-Push"].[0]) - } - - [] - let ``withHxRedirect succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxRedirect "/somewhere-else" next ctx - Assert.True (dic.ContainsKey "HX-Redirect") - Assert.Equal ("/somewhere-else", dic.["HX-Redirect"].[0]) - } - - [] - let ``withHxRefresh succeeds when set to true`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxRefresh true next ctx - Assert.True (dic.ContainsKey "HX-Refresh") - Assert.Equal ("true", dic.["HX-Refresh"].[0]) - } - - [] - let ``withHxRefresh succeeds when set to false`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxRefresh false next ctx - Assert.True (dic.ContainsKey "HX-Refresh") - Assert.Equal ("false", dic.["HX-Refresh"].[0]) - } - - [] - let ``withHxRetarget succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxRetarget "#somewhereElse" next ctx - Assert.True (dic.ContainsKey "HX-Retarget") - Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0]) - } + open System.Threading.Tasks - [] - let ``withHxTrigger succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxTrigger "doSomething" next ctx - Assert.True (dic.ContainsKey "HX-Trigger") - Assert.Equal ("doSomething", dic.["HX-Trigger"].[0]) - } + /// Dummy "next" parameter to get the pipeline to execute/terminate + let next (ctx : HttpContext) = Task.FromResult (Some ctx) + + [] + let ``withHxPushUrl succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxPushUrl "/a-new-url" next ctx + Assert.True (dic.ContainsKey "HX-Push-Url") + Assert.Equal ("/a-new-url", dic["HX-Push-Url"][0]) + } - [] - let ``withHxTriggerMany succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx - Assert.True (dic.ContainsKey "HX-Trigger") - Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic.["HX-Trigger"].[0]) - } + [] + let ``withHxNoPushUrl succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxNoPushUrl next ctx + Assert.True (dic.ContainsKey "HX-Push-Url") + Assert.Equal ("false", dic["HX-Push-Url"][0]) + } - [] - let ``withHxTriggerAfterSettle succeeds`` () = - let ctx = Substitute.For () - let dic = HeaderDictionary () - ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore - task { - let! _ = withHxTriggerAfterSettle "byTheWay" next ctx - Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") - Assert.Equal ("byTheWay", dic.["HX-Trigger-After-Settle"].[0]) - } + [] + let ``withHxRedirect succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRedirect "/somewhere-else" next ctx + Assert.True (dic.ContainsKey "HX-Redirect") + Assert.Equal ("/somewhere-else", dic["HX-Redirect"][0]) + } - [] - let ``withHxTriggerManyAfterSettle succeeds`` () = - let ctx = Substitute.For () - 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]) - } + [] + let ``withHxRefresh succeeds when set to true`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRefresh true next ctx + Assert.True (dic.ContainsKey "HX-Refresh") + Assert.Equal ("true", dic["HX-Refresh"][0]) + } - [] - let ``withHxTriggerAfterSwap succeeds`` () = - let ctx = Substitute.For () - 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]) - } + [] + let ``withHxRefresh succeeds when set to false`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRefresh false next ctx + Assert.True (dic.ContainsKey "HX-Refresh") + Assert.Equal ("false", dic["HX-Refresh"][0]) + } - [] - let ``withHxTriggerManyAfterSwap succeeds`` () = - let ctx = Substitute.For () - 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]) - } + [] + let ``withHxReplaceUrl succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxReplaceUrl "/a-substitute-url" next ctx + Assert.True (dic.ContainsKey "HX-Replace-Url") + Assert.Equal ("/a-substitute-url", dic["HX-Replace-Url"][0]) + } + [] + let ``withHxNoReplaceUrl succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxNoReplaceUrl next ctx + Assert.True (dic.ContainsKey "HX-Replace-Url") + Assert.Equal ("false", dic["HX-Replace-Url"][0]) + } + + [] + let ``withHxReswap succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxReswap HxSwap.BeforeEnd next ctx + Assert.True (dic.ContainsKey "HX-Reswap") + Assert.Equal (HxSwap.BeforeEnd, dic["HX-Reswap"][0]) + } + + [] + let ``withHxRetarget succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxRetarget "#somewhereElse" next ctx + Assert.True (dic.ContainsKey "HX-Retarget") + Assert.Equal ("#somewhereElse", dic["HX-Retarget"][0]) + } + + [] + let ``withHxTrigger succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTrigger "doSomething" next ctx + Assert.True (dic.ContainsKey "HX-Trigger") + Assert.Equal ("doSomething", dic["HX-Trigger"][0]) + } + + [] + let ``withHxTriggerMany succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx + Assert.True (dic.ContainsKey "HX-Trigger") + Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic["HX-Trigger"][0]) + } + + [] + let ``withHxTriggerAfterSettle succeeds`` () = + let ctx = Substitute.For () + let dic = HeaderDictionary () + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + task { + let! _ = withHxTriggerAfterSettle "byTheWay" next ctx + Assert.True (dic.ContainsKey "HX-Trigger-After-Settle") + Assert.Equal ("byTheWay", dic["HX-Trigger-After-Settle"][0]) + } + + [] + let ``withHxTriggerManyAfterSettle succeeds`` () = + let ctx = Substitute.For () + 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]) + } + + [] + let ``withHxTriggerAfterSwap succeeds`` () = + let ctx = Substitute.For () + 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]) + } + + [] + let ``withHxTriggerManyAfterSwap succeeds`` () = + let ctx = Substitute.For () + 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]) + } diff --git a/src/Htmx/Giraffe.Htmx.fsproj b/src/Htmx/Giraffe.Htmx.fsproj index 6063798..09928ad 100644 --- a/src/Htmx/Giraffe.Htmx.fsproj +++ b/src/Htmx/Giraffe.Htmx.fsproj @@ -1,7 +1,6 @@  - net6.0 true htmx header extensions and helpers for Giraffe README.md @@ -16,4 +15,8 @@ + + + + diff --git a/src/Htmx/Htmx.fs b/src/Htmx/Htmx.fs index 51489a5..ff7f1e0 100644 --- a/src/Htmx/Htmx.fs +++ b/src/Htmx/Htmx.fs @@ -6,102 +6,122 @@ open System /// Determine if the given header is present 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 type IHeaderDictionary with - /// Indicates that the request is via an element using `hx-boost` - 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)_ - 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 - member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse - - /// The user response to an `hx-prompt` - member this.HxPrompt with get () = hdr this "HX-Prompt" + /// Indicates that the request is via an element using `hx-boost` + 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)_ + 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 + member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse + + /// The user response to an `hx-prompt` + member this.HxPrompt with get () = hdr this "HX-Prompt" - /// `true` if the request came from HTMX - member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse + /// `true` if the request came from HTMX + member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse - /// The `id` of the target element if it exists - member this.HxTarget with get () = hdr this "HX-Target" + /// The `id` of the target element if it exists + member this.HxTarget with get () = hdr this "HX-Target" - /// The `id` of the triggered element if it exists - member this.HxTrigger with get () = hdr this "HX-Trigger" + /// The `id` of the triggered element if it exists + member this.HxTrigger with get () = hdr this "HX-Trigger" - /// The `name` of the triggered element if it exists - member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" + /// The `name` of the triggered element if it exists + member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" /// Extensions for the request object type HttpRequest with - /// Whether this request was initiated from htmx - member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false + /// Whether this request was initiated from htmx + member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false - /// Whether this request is an htmx history-miss refresh request - member this.IsHtmxRefresh with get () = - this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false) + /// Whether this request is an htmx history-miss refresh request + member this.IsHtmxRefresh with get () = + this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false) /// HTTP handlers for setting output headers [] module Handlers = - /// Convert a boolean to lowercase `true` or `false` - let private toLowerBool (trueOrFalse : bool) = - (string trueOrFalse).ToLowerInvariant () + /// Convert a boolean to lowercase `true` or `false` + let private toLowerBool (trueOrFalse : bool) = + (string trueOrFalse).ToLowerInvariant () - /// Serialize a list of key/value pairs to JSON (very rudimentary) - let private toJson (evts : (string * string) list) = - evts - |> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\""))) - |> String.concat ", " - |> sprintf "{ %s }" + /// Serialize a list of key/value pairs to JSON (very rudimentary) + let private toJson (evts : (string * string) list) = + evts + |> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\""))) + |> String.concat ", " + |> sprintf "{ %s }" - // Pushes a new url into the history stack - let withHxPush : string -> HttpHandler = - setHttpHeader "HX-Push" + /// Pushes a new url into the history stack + let withHxPushUrl : string -> HttpHandler = + setHttpHeader "HX-Push-Url" - // Explicitly do not push a new URL into the history stack - let withHxNoPush : HttpHandler = - toLowerBool false |> withHxPush + /// Explicitly do not push a new URL into the history stack + let withHxNoPushUrl : HttpHandler = + toLowerBool false |> withHxPushUrl + + /// Pushes a new url into the history stack + [] + let withHxPush = withHxPushUrl - /// Can be used to do a client-side redirect to a new location - let withHxRedirect : string -> HttpHandler = - setHttpHeader "HX-Redirect" + /// Explicitly do not push a new URL into the history stack + [] + let withHxNoPush = withHxNoPushUrl + + /// Can be used to do a client-side redirect to a new location + let withHxRedirect : string -> HttpHandler = + setHttpHeader "HX-Redirect" - /// If set to `true` the client side will do a a full refresh of the page - let withHxRefresh : bool -> HttpHandler = - toLowerBool >> setHttpHeader "HX-Refresh" + /// If set to `true` the client side will do a a full refresh of the page + let withHxRefresh : bool -> HttpHandler = + toLowerBool >> setHttpHeader "HX-Refresh" - /// Allows you to override the `hx-target` attribute - let withHxRetarget : string -> HttpHandler = - setHttpHeader "HX-Retarget" + /// Replaces the current URL in the history stack + let withHxReplaceUrl : string -> HttpHandler = + setHttpHeader "HX-Replace-Url" - /// Allows you to trigger a single client side event - let withHxTrigger : string -> HttpHandler = - setHttpHeader "HX-Trigger" + /// Explicitly do not replace the current URL in the history stack + let withHxNoReplaceUrl : HttpHandler = + toLowerBool false |> withHxReplaceUrl + + /// Override the `hx-swap` attribute from the initiating element + let withHxReswap : string -> HttpHandler = + setHttpHeader "HX-Reswap" - /// Allows you to trigger multiple client side events - let withHxTriggerMany evts : HttpHandler = - toJson evts |> setHttpHeader "HX-Trigger" + /// Allows you to override the `hx-target` attribute + let withHxRetarget : string -> HttpHandler = + setHttpHeader "HX-Retarget" - /// 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 a single client side event + let withHxTrigger : string -> HttpHandler = + setHttpHeader "HX-Trigger" - /// 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 multiple client side events + let withHxTriggerMany evts : HttpHandler = + toJson evts |> setHttpHeader "HX-Trigger" - /// 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 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 DOM swapping occurs - let withHxTriggerManyAfterSwap evts : HttpHandler = - toJson evts |> setHttpHeader "HX-Trigger-After-Swap" + /// 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" diff --git a/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj index 2d275f3..9d9d736 100644 --- a/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj +++ b/src/ViewEngine.Htmx.Tests/Giraffe.ViewEngine.Htmx.Tests.fsproj @@ -1,8 +1,6 @@ - net6.0 - false false diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs index 86ed2cf..c9a4a5b 100644 --- a/src/ViewEngine.Htmx.Tests/Tests.fs +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -5,463 +5,441 @@ open Xunit /// Tests for the HxEncoding module module Encoding = - - [] - let ``Form is correct`` () = - Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form) - - [] - let ``MultipartForm is correct`` () = - Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm) + + [] + let ``Form is correct`` () = + Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form) + + [] + let ``MultipartForm is correct`` () = + Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm) /// Tests for the HxHeaders module module Headers = - - [] - let ``From succeeds with an empty list`` () = - Assert.Equal ("{ }", HxHeaders.From []) - - [] - let ``From succeeds and escapes quotes`` () = - Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }", - HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ]) + + [] + let ``From succeeds with an empty list`` () = + Assert.Equal ("{ }", HxHeaders.From []) + + [] + let ``From succeeds and escapes quotes`` () = + Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }", + HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ]) /// Tests for the HxParams module module Params = - - [] - let ``All is correct`` () = - Assert.Equal ("*", HxParams.All) - - [] - let ``None is correct`` () = - Assert.Equal ("none", HxParams.None) - [] - let ``With succeeds with empty list`` () = - Assert.Equal ("", HxParams.With []) - - [] - let ``With succeeds with one list item`` () = - Assert.Equal ("boo", HxParams.With [ "boo" ]) - - [] - let ``With succeeds with multiple list items`` () = - Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ]) + [] + let ``All is correct`` () = + Assert.Equal ("*", HxParams.All) - [] - let ``Except succeeds with empty list`` () = - Assert.Equal ("not ", HxParams.Except []) - - [] - let ``Except succeeds with one list item`` () = - Assert.Equal ("not that", HxParams.Except [ "that" ]) - - [] - let ``Except succeeds with multiple list items`` () = - Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ]) + [] + let ``None is correct`` () = + Assert.Equal ("none", HxParams.None) + + [] + let ``With succeeds with empty list`` () = + Assert.Equal ("", HxParams.With []) + + [] + let ``With succeeds with one list item`` () = + Assert.Equal ("boo", HxParams.With [ "boo" ]) + + [] + let ``With succeeds with multiple list items`` () = + Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ]) + + [] + let ``Except succeeds with empty list`` () = + Assert.Equal ("not ", HxParams.Except []) + + [] + let ``Except succeeds with one list item`` () = + Assert.Equal ("not that", HxParams.Except [ "that" ]) + + [] + let ``Except succeeds with multiple list items`` () = + Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ]) /// Tests for the HxRequest module module Request = - - [] - let ``Configure succeeds with an empty list`` () = - Assert.Equal ("{ }", HxRequest.Configure []) - - [] - let ``Configure succeeds with a non-empty list`` () = - Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) - - [] - let ``Configure succeeds with all known params configured`` () = - Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }", - HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ]) - - [] - let ``Timeout succeeds`` () = - Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50) - - [] - let ``Credentials succeeds when set to true`` () = - Assert.Equal ("\"credentials\": true", HxRequest.Credentials true) - - [] - let ``Credentials succeeds when set to false`` () = - Assert.Equal ("\"credentials\": false", HxRequest.Credentials false) - - [] - let ``NoHeaders succeeds when set to true`` () = - Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true) - - [] - let ``NoHeaders succeeds when set to false`` () = - Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false) + + [] + let ``Configure succeeds with an empty list`` () = + Assert.Equal ("{ }", HxRequest.Configure []) + + [] + let ``Configure succeeds with a non-empty list`` () = + Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) + + [] + let ``Configure succeeds with all known params configured`` () = + Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }", + HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ]) + + [] + let ``Timeout succeeds`` () = + Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50) + + [] + let ``Credentials succeeds when set to true`` () = + Assert.Equal ("\"credentials\": true", HxRequest.Credentials true) + + [] + let ``Credentials succeeds when set to false`` () = + Assert.Equal ("\"credentials\": false", HxRequest.Credentials false) + + [] + let ``NoHeaders succeeds when set to true`` () = + Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true) + + [] + let ``NoHeaders succeeds when set to false`` () = + Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false) -/// Tests for the HxSwap module -module Swap = - - [] - let ``InnerHtml is correct`` () = - Assert.Equal ("innerHTML", HxSwap.InnerHtml) - - [] - let ``OuterHtml is correct`` () = - Assert.Equal ("outerHTML", HxSwap.OuterHtml) - - [] - let ``BeforeBegin is correct`` () = - Assert.Equal ("beforebegin", HxSwap.BeforeBegin) - - [] - let ``BeforeEnd is correct`` () = - Assert.Equal ("beforeend", HxSwap.BeforeEnd) - - [] - let ``AfterBegin is correct`` () = - Assert.Equal ("afterbegin", HxSwap.AfterBegin) - - [] - let ``AfterEnd is correct`` () = - Assert.Equal ("afterend", HxSwap.AfterEnd) - - [] - let ``None is correct`` () = - Assert.Equal ("none", HxSwap.None) - - /// Tests for the HxTrigger module module Trigger = - - [] - let ``Click is correct`` () = - Assert.Equal ("click", HxTrigger.Click) - - [] - let ``Load is correct`` () = - Assert.Equal ("load", HxTrigger.Load) - - [] - let ``Revealed is correct`` () = - Assert.Equal ("revealed", HxTrigger.Revealed) - - [] - let ``Every succeeds`` () = - Assert.Equal ("every 3s", HxTrigger.Every "3s") - [] - let ``Filter.Alt succeeds`` () = - Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click) - - [] - let ``Filter.Ctrl succeeds`` () = - Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click) - - [] - let ``Filter.Shift succeeds`` () = - Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click) - - [] - let ``Filter.CtrlAlt succeeds`` () = - Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click) + [] + let ``Click is correct`` () = + Assert.Equal ("click", HxTrigger.Click) - [] - let ``Filter.CtrlShift succeeds`` () = - Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click) + [] + let ``Load is correct`` () = + Assert.Equal ("load", HxTrigger.Load) - [] - let ``Filter.CtrlAltShift succeeds`` () = - Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click) + [] + let ``Revealed is correct`` () = + Assert.Equal ("revealed", HxTrigger.Revealed) - [] - let ``Filter.AltShift succeeds`` () = - Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click) + [] + let ``Every succeeds`` () = + Assert.Equal ("every 3s", HxTrigger.Every "3s") - [] - let ``Once succeeds when it is the first modifier`` () = - Assert.Equal ("once", HxTrigger.Once "") - - [] - let ``Once succeeds when it is not the first modifier`` () = - Assert.Equal ("click once", HxTrigger.Once "click") + [] + let ``Filter.Alt succeeds`` () = + Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click) - [] - let ``Changed succeeds when it is the first modifier`` () = - Assert.Equal ("changed", HxTrigger.Changed "") - - [] - let ``Changed succeeds when it is not the first modifier`` () = - Assert.Equal ("click changed", HxTrigger.Changed "click") + [] + let ``Filter.Ctrl succeeds`` () = + Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click) - [] - let ``Delay succeeds when it is the first modifier`` () = - Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "") - - [] - let ``Delay succeeds when it is not the first modifier`` () = - Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click") + [] + let ``Filter.Shift succeeds`` () = + Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click) - [] - let ``Throttle succeeds when it is the first modifier`` () = - Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "") - - [] - let ``Throttle succeeds when it is not the first modifier`` () = - Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click") + [] + let ``Filter.CtrlAlt succeeds`` () = + Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click) - [] - let ``From succeeds when it is the first modifier`` () = - Assert.Equal ("from:.nav", HxTrigger.From ".nav" "") - - [] - let ``From succeeds when it is not the first modifier`` () = - Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click") + [] + let ``Filter.CtrlShift succeeds`` () = + Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click) - [] - let ``FromDocument succeeds when it is the first modifier`` () = - Assert.Equal ("from:document", HxTrigger.FromDocument "") - - [] - let ``FromDocument succeeds when it is not the first modifier`` () = - Assert.Equal ("click from:document", HxTrigger.FromDocument "click") + [] + let ``Filter.CtrlAltShift succeeds`` () = + Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click) - [] - let ``FromWindow succeeds when it is the first modifier`` () = - Assert.Equal ("from:window", HxTrigger.FromWindow "") - - [] - let ``FromWindow succeeds when it is not the first modifier`` () = - Assert.Equal ("click from:window", HxTrigger.FromWindow "click") + [] + let ``Filter.AltShift succeeds`` () = + Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click) - [] - let ``FromClosest succeeds when it is the first modifier`` () = - Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "") - - [] - let ``FromClosest succeeds when it is not the first modifier`` () = - Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click") + [] + let ``Once succeeds when it is the first modifier`` () = + Assert.Equal ("once", HxTrigger.Once "") - [] - let ``FromFind succeeds when it is the first modifier`` () = - Assert.Equal ("from:find li", HxTrigger.FromFind "li" "") - - [] - let ``FromFind succeeds when it is not the first modifier`` () = - Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click") + [] + let ``Once succeeds when it is not the first modifier`` () = + Assert.Equal ("click once", HxTrigger.Once "click") - [] - let ``Target succeeds when it is the first modifier`` () = - Assert.Equal ("target:main", HxTrigger.Target "main" "") - - [] - let ``Target succeeds when it is not the first modifier`` () = - Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click") + [] + let ``Changed succeeds when it is the first modifier`` () = + Assert.Equal ("changed", HxTrigger.Changed "") - [] - let ``Consume succeeds when it is the first modifier`` () = - Assert.Equal ("consume", HxTrigger.Consume "") - - [] - let ``Consume succeeds when it is not the first modifier`` () = - Assert.Equal ("click consume", HxTrigger.Consume "click") + [] + let ``Changed succeeds when it is not the first modifier`` () = + Assert.Equal ("click changed", HxTrigger.Changed "click") - [] - let ``Queue succeeds when it is the first modifier`` () = - Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "") - - [] - let ``Queue succeeds when it is not the first modifier`` () = - Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click") + [] + let ``Delay succeeds when it is the first modifier`` () = + Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "") - [] - let ``QueueFirst succeeds when it is the first modifier`` () = - Assert.Equal ("queue:first", HxTrigger.QueueFirst "") - - [] - let ``QueueFirst succeeds when it is not the first modifier`` () = - Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click") + [] + let ``Delay succeeds when it is not the first modifier`` () = + Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click") - [] - let ``QueueLast succeeds when it is the first modifier`` () = - Assert.Equal ("queue:last", HxTrigger.QueueLast "") - - [] - let ``QueueLast succeeds when it is not the first modifier`` () = - Assert.Equal ("click queue:last", HxTrigger.QueueLast "click") + [] + let ``Throttle succeeds when it is the first modifier`` () = + Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "") - [] - let ``QueueAll succeeds when it is the first modifier`` () = - Assert.Equal ("queue:all", HxTrigger.QueueAll "") - - [] - let ``QueueAll succeeds when it is not the first modifier`` () = - Assert.Equal ("click queue:all", HxTrigger.QueueAll "click") + [] + let ``Throttle succeeds when it is not the first modifier`` () = + Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click") - [] - let ``QueueNone succeeds when it is the first modifier`` () = - Assert.Equal ("queue:none", HxTrigger.QueueNone "") - - [] - let ``QueueNone succeeds when it is not the first modifier`` () = - Assert.Equal ("click queue:none", HxTrigger.QueueNone "click") + [] + let ``From succeeds when it is the first modifier`` () = + Assert.Equal ("from:.nav", HxTrigger.From ".nav" "") + + [] + let ``From succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click") + + [] + let ``FromDocument succeeds when it is the first modifier`` () = + Assert.Equal ("from:document", HxTrigger.FromDocument "") + + [] + let ``FromDocument succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:document", HxTrigger.FromDocument "click") + + [] + let ``FromWindow succeeds when it is the first modifier`` () = + Assert.Equal ("from:window", HxTrigger.FromWindow "") + + [] + let ``FromWindow succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:window", HxTrigger.FromWindow "click") + + [] + let ``FromClosest succeeds when it is the first modifier`` () = + Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "") + + [] + let ``FromClosest succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click") + + [] + let ``FromFind succeeds when it is the first modifier`` () = + Assert.Equal ("from:find li", HxTrigger.FromFind "li" "") + + [] + let ``FromFind succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click") + + [] + let ``Target succeeds when it is the first modifier`` () = + Assert.Equal ("target:main", HxTrigger.Target "main" "") + + [] + let ``Target succeeds when it is not the first modifier`` () = + Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click") + + [] + let ``Consume succeeds when it is the first modifier`` () = + Assert.Equal ("consume", HxTrigger.Consume "") + + [] + let ``Consume succeeds when it is not the first modifier`` () = + Assert.Equal ("click consume", HxTrigger.Consume "click") + + [] + let ``Queue succeeds when it is the first modifier`` () = + Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "") + + [] + let ``Queue succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click") + + [] + let ``QueueFirst succeeds when it is the first modifier`` () = + Assert.Equal ("queue:first", HxTrigger.QueueFirst "") + + [] + let ``QueueFirst succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click") + + [] + let ``QueueLast succeeds when it is the first modifier`` () = + Assert.Equal ("queue:last", HxTrigger.QueueLast "") + + [] + let ``QueueLast succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:last", HxTrigger.QueueLast "click") + + [] + let ``QueueAll succeeds when it is the first modifier`` () = + Assert.Equal ("queue:all", HxTrigger.QueueAll "") + + [] + let ``QueueAll succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:all", HxTrigger.QueueAll "click") + + [] + let ``QueueNone succeeds when it is the first modifier`` () = + Assert.Equal ("queue:none", HxTrigger.QueueNone "") + + [] + let ``QueueNone succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:none", HxTrigger.QueueNone "click") /// Tests for the HxVals module module Vals = - - [] - let ``From succeeds with an empty list`` () = - Assert.Equal ("{ }", HxVals.From []) - - [] - let ``From succeeds and escapes quotes`` () = - Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }", - HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ]) + + [] + let ``From succeeds with an empty list`` () = + Assert.Equal ("{ }", HxVals.From []) + + [] + let ``From succeeds and escapes quotes`` () = + Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }", + HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ]) /// Tests for the HtmxAttrs module module Attributes = - - /// Pipe-able assertion for a rendered node - let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node) - - [] - let ``_hxBoost succeeds`` () = - div [ _hxBoost ] [] |> shouldRender """
""" - [] - let ``_hxConfirm succeeds`` () = - button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """""" + /// Pipe-able assertion for a rendered node + let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node) - [] - let ``_hxDelete succeeds`` () = - span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """""" - - [] - let ``_hxDisable succeeds`` () = - p [ _hxDisable ] [] |> shouldRender """

""" - - [] - let ``_hxDisinherit succeeds`` () = - strong [ _hxDisinherit "*" ] [] |> shouldRender """""" - - [] - let ``_hxEncoding succeeds`` () = - form [ _hxEncoding "utf-7" ] [] |> shouldRender """
""" - - [] - let ``_hxExt succeeds`` () = - section [ _hxExt "extendme" ] [] |> shouldRender """
""" - - [] - let ``_hxGet succeeds`` () = - article [ _hxGet "/the-text" ] [] |> shouldRender """
""" - - [] - let ``_hxHeaders succeeds`` () = - figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] - |> shouldRender """
""" - - [] - let ``_hxHistoryElt succeeds`` () = - table [ _hxHistoryElt ] [] |> shouldRender """
""" - - [] - let ``_hxInclude succeeds`` () = - a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """""" - - [] - let ``_hxIndicator succeeds`` () = - aside [ _hxIndicator "#spinner" ] [] |> shouldRender """""" - - [] - let ``_hxNoBoost succeeds`` () = - td [ _hxNoBoost ] [] |> shouldRender """""" - - [] - let ``_hxParams succeeds`` () = - br [ _hxParams "[p1,p2]" ] |> shouldRender """
""" - - [] - let ``_hxPatch succeeds`` () = - div [ _hxPatch "/arrrgh" ] [] |> shouldRender """
""" - - [] - let ``_hxPost succeeds`` () = - hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
""" - - [] - let ``_hxPreserve succeeds`` () = - img [ _hxPreserve ] |> shouldRender """""" - - [] - let ``_hxPrompt succeeds`` () = - strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """""" - - [] - let ``_hxPushUrl succeeds`` () = - dl [ _hxPushUrl ] [] |> shouldRender """
""" - - [] - let ``_hxPut succeeds`` () = - s [ _hxPut "/take-this" ] [] |> shouldRender """""" - - [] - let ``_hxRequest succeeds`` () = - u [ _hxRequest "noHeaders" ] [] |> shouldRender """""" - - [] - let ``_hxSelect succeeds`` () = - nav [ _hxSelect "#navbar" ] [] |> shouldRender """""" - - [] - let ``_hxSse succeeds`` () = - footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """
""" - - [] - let ``_hxSwap succeeds`` () = - del [ _hxSwap "innerHTML" ] [] |> shouldRender """""" + [] + let ``_hxBoost succeeds`` () = + div [ _hxBoost ] [] |> shouldRender """
""" - [] - let ``_hxSwapOob succeeds`` () = - li [ _hxSwapOob "true" ] [] |> shouldRender """
  • """ - - [] - let ``_hxSync succeeds`` () = - nav [ _hxSync "closest form:abort" ] [] |> shouldRender """""" + [] + let ``_hxConfirm succeeds`` () = + button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """""" - [] - let ``_hxTarget succeeds`` () = - header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
    """ + [] + let ``_hxDelete succeeds`` () = + span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """""" - [] - let ``_hxTrigger succeeds`` () = - figcaption [ _hxTrigger "load" ] [] |> shouldRender """
    """ - - [] - let ``_hxVals succeeds`` () = - dt [ _hxVals """{ "extra": "values" }""" ] [] - |> shouldRender """
    """ - - [] - let ``_hxWs succeeds`` () = - ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """
      """ + [] + let ``_hxDisable succeeds`` () = + p [ _hxDisable ] [] |> shouldRender """

      """ + + [] + let ``_hxDisinherit succeeds`` () = + strong [ _hxDisinherit "*" ] [] |> shouldRender """""" + + [] + let ``_hxEncoding succeeds`` () = + form [ _hxEncoding "utf-7" ] [] |> shouldRender """
      """ + + [] + let ``_hxExt succeeds`` () = + section [ _hxExt "extendme" ] [] |> shouldRender """
      """ + + [] + let ``_hxGet succeeds`` () = + article [ _hxGet "/the-text" ] [] |> shouldRender """
      """ + + [] + let ``_hxHeaders succeeds`` () = + figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] [] + |> shouldRender """
      """ + + [] + let ``_hxHistoryElt succeeds`` () = + table [ _hxHistoryElt ] [] |> shouldRender """
      """ + + [] + let ``_hxInclude succeeds`` () = + a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """""" + + [] + let ``_hxIndicator succeeds`` () = + aside [ _hxIndicator "#spinner" ] [] |> shouldRender """""" + + [] + let ``_hxNoBoost succeeds`` () = + td [ _hxNoBoost ] [] |> shouldRender """""" + + [] + let ``_hxParams succeeds`` () = + br [ _hxParams "[p1,p2]" ] |> shouldRender """
      """ + + [] + let ``_hxPatch succeeds`` () = + div [ _hxPatch "/arrrgh" ] [] |> shouldRender """
      """ + + [] + let ``_hxPost succeeds`` () = + hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
      """ + + [] + let ``_hxPreserve succeeds`` () = + img [ _hxPreserve ] |> shouldRender """""" + + [] + let ``_hxPrompt succeeds`` () = + strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """""" + + [] + let ``_hxPushUrl succeeds`` () = + dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """
      """ + + [] + let ``_hxPut succeeds`` () = + s [ _hxPut "/take-this" ] [] |> shouldRender """""" + + [] + let ``_hxReplaceUrl succeeds`` () = + p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """

      """ + + [] + let ``_hxRequest succeeds`` () = + u [ _hxRequest "noHeaders" ] [] |> shouldRender """""" + + [] + let ``_hxSelect succeeds`` () = + nav [ _hxSelect "#navbar" ] [] |> shouldRender """""" + + [] + let ``_hxSelectOob succeeds`` () = + section [ _hxSelectOob "#oob" ] [] |> shouldRender """
      """ + + [] + let ``_hxSse succeeds`` () = + footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """
      """ + + [] + let ``_hxSwap succeeds`` () = + del [ _hxSwap "innerHTML" ] [] |> shouldRender """""" + + [] + let ``_hxSwapOob succeeds`` () = + li [ _hxSwapOob "true" ] [] |> shouldRender """
    • """ + + [] + let ``_hxSync succeeds`` () = + nav [ _hxSync "closest form:abort" ] [] |> shouldRender """""" + + [] + let ``_hxTarget succeeds`` () = + header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
      """ + + [] + let ``_hxTrigger succeeds`` () = + figcaption [ _hxTrigger "load" ] [] |> shouldRender """
      """ + + [] + let ``_hxVals succeeds`` () = + dt [ _hxVals """{ "extra": "values" }""" ] [] + |> shouldRender """
      """ + + [] + let ``_hxWs succeeds`` () = + ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """
        """ /// Tests for the Script module module Script = - - [] - let ``Script.minified succeeds`` () = - let html = RenderView.AsString.htmlNode Script.minified - Assert.Equal ("""""", - html) - - [] - let ``Script.unminified succeeds`` () = - let html = RenderView.AsString.htmlNode Script.unminified - Assert.Equal ("""""", - html) + + [] + let ``Script.minified succeeds`` () = + let html = RenderView.AsString.htmlNode Script.minified + Assert.Equal + ("""""", + html) + + [] + let ``Script.unminified succeeds`` () = + let html = RenderView.AsString.htmlNode Script.unminified + Assert.Equal + ("""""", + html) diff --git a/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj b/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj index 784f743..665b85c 100644 --- a/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj +++ b/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj @@ -1,7 +1,6 @@  - net6.0 true Extensions to Giraffe View Engine to support htmx attributes and their values README.md @@ -16,4 +15,8 @@ + + + + diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index ff3e606..51a948d 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -2,237 +2,293 @@ /// Serialize a list of key/value pairs to JSON (very rudimentary) let private toJson (kvps : (string * string) list) = - kvps - |> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\""))) - |> String.concat ", " - |> sprintf "{ %s }" + kvps + |> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\""))) + |> String.concat ", " + |> sprintf "{ %s }" /// Valid values for the `hx-encoding` attribute [] module HxEncoding = - /// A standard HTTP form - let Form = "application/x-www-form-urlencoded" - /// A multipart form (used for file uploads) - let MultipartForm = "multipart/form-data" + + /// A standard HTTP form + let Form = "application/x-www-form-urlencoded" + + /// A multipart form (used for file uploads) + let MultipartForm = "multipart/form-data" /// Helper to create the `hx-headers` attribute [] 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 [] module HxParams = - /// Include all parameters - let All = "*" - /// Include no parameters - let None = "none" - /// 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" + + /// Include all parameters + let All = "*" + + /// Include no parameters + let None = "none" + + /// 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 [] 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" - - -/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config) -[] -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" + + /// 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" /// Helpers for the `hx-trigger` attribute [] module HxTrigger = - /// Append a filter to a trigger - let private appendFilter filter (trigger : string) = - match trigger.Contains "[" with - | true -> - let parts = trigger.Split ('[', ']') - $"{parts.[0]}[{parts.[1]}&&{filter}]" - | false -> $"{trigger}[{filter}]" - /// Trigger the event on a click - let Click = "click" - /// Trigger the event on page load - let Load = "load" - /// Trigger the event when the item is visible - let Revealed = "revealed" - /// Trigger this event every [timing declaration] - let Every (duration : string) = $"every {duration}" - /// Helpers for defining filters - module Filter = - /// Only trigger the event if the `ALT` key is pressed - let Alt = appendFilter "altKey" - /// Only trigger the event if the `CTRL` key is pressed - let Ctrl = appendFilter "ctrlKey" - /// Only trigger the event if the `SHIFT` key is pressed - let Shift = appendFilter "shiftKey" - /// Only trigger the event if `CTRL+ALT` are pressed - let CtrlAlt = Ctrl >> Alt - /// Only trigger the event if `CTRL+SHIFT` are pressed - let CtrlShift = Ctrl >> Shift - /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed - let CtrlAltShift = CtrlAlt >> Shift - /// Only trigger the event if `ALT+SHIFT` are pressed - let AltShift = Alt >> Shift - /// Append a modifier to the current trigger - let private appendModifier modifier current = - match current with "" -> modifier | _ -> $"{current} {modifier}" - /// Only trigger once - let Once = appendModifier "once" - /// Trigger when changed - let Changed = appendModifier "changed" - /// Delay execution; resets every time the event is seen - let Delay = sprintf "delay:%s" >> appendModifier - /// Throttle execution; ignore other events, fire when duration passes - let Throttle = sprintf "throttle:%s" >> appendModifier - /// Trigger this event from a CSS selector - let From = sprintf "from:%s" >> appendModifier - /// Trigger this event from the `document` object - 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" + + /// Append a filter to a trigger + let private appendFilter filter (trigger : string) = + match trigger.Contains "[" with + | true -> + let parts = trigger.Split ('[', ']') + $"{parts[0]}[{parts[1]}&&{filter}]" + | false -> $"{trigger}[{filter}]" + + /// Trigger the event on a click + let Click = "click" + + /// Trigger the event on page load + let Load = "load" + + /// Trigger the event when the item is visible + let Revealed = "revealed" + + /// Trigger this event every [timing declaration] + let Every (duration : string) = $"every {duration}" + + /// Helpers for defining filters + module Filter = + + /// Only trigger the event if the `ALT` key is pressed + let Alt = appendFilter "altKey" + + /// Only trigger the event if the `CTRL` key is pressed + let Ctrl = appendFilter "ctrlKey" + + /// Only trigger the event if the `SHIFT` key is pressed + let Shift = appendFilter "shiftKey" + + /// Only trigger the event if `CTRL+ALT` are pressed + let CtrlAlt = Ctrl >> Alt + + /// Only trigger the event if `CTRL+SHIFT` are pressed + let CtrlShift = Ctrl >> Shift + + /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed + let CtrlAltShift = CtrlAlt >> Shift + + /// Only trigger the event if `ALT+SHIFT` are pressed + let AltShift = Alt >> Shift + + /// Append a modifier to the current trigger + let private appendModifier modifier current = + if current = "" then modifier else $"{current} {modifier}" + + /// Only trigger once + let Once = appendModifier "once" + + /// Trigger when changed + let Changed = appendModifier "changed" + + /// Delay execution; resets every time the event is seen + let Delay = sprintf "delay:%s" >> appendModifier + + /// Throttle execution; ignore other events, fire when duration passes + let Throttle = sprintf "throttle:%s" >> appendModifier + + /// Trigger this event from a CSS selector + let From = sprintf "from:%s" >> appendModifier + + /// Trigger this event from the `document` object + 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 [] 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 [] module HtmxAttrs = - /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false) - let _hxBoost = attr "hx-boost" "true" - /// Shows a confirm() dialog before issuing a request - let _hxConfirm = attr "hx-confirm" - /// Issues a DELETE to the specified URL - let _hxDelete = attr "hx-delete" - /// Disables htmx processing for the given node and any children nodes - let _hxDisable = flag "hx-disable" - /// Disinherit all ("*") or specific htmx attributes - let _hxDisinherit = attr "hx-disinherit" - /// Changes the request encoding type - let _hxEncoding = attr "hx-encoding" - /// Extensions to use for this element - let _hxExt = attr "hx-ext" - /// Issues a GET to the specified URL - let _hxGet = attr "hx-get" - /// Adds to the headers that will be submitted with the request - let _hxHeaders = attr "hx-headers" - /// The element to snapshot and restore during history navigation - let _hxHistoryElt = flag "hx-history-elt" - /// Includes additional data in AJAX requests - let _hxInclude = attr "hx-include" - /// The element to put the htmx-request class on during the AJAX request - let _hxIndicator = attr "hx-indicator" - /// Overrides a previous `hx-boost` - let _hxNoBoost = attr "hx-boost" "false" - /// Filters the parameters that will be submitted with a request - let _hxParams = attr "hx-params" - /// Issues a PATCH to the specified URL - let _hxPatch = attr "hx-patch" - /// Issues a POST to the specified URL - let _hxPost = attr "hx-post" - /// Preserves an element between requests - let _hxPreserve = attr "hx-preserve" "true" - /// Shows a prompt before submitting a request - let _hxPrompt = attr "hx-prompt" - /// Pushes the URL into the location bar, creating a new history entry - let _hxPushUrl = flag "hx-push-url" - /// Issues a PUT to the specified URL - let _hxPut = attr "hx-put" - /// 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" - /// 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" - /// 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" + + /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false) + let _hxBoost = attr "hx-boost" "true" + + /// Shows a confirm() dialog before issuing a request + let _hxConfirm = attr "hx-confirm" + + /// Issues a DELETE to the specified URL + let _hxDelete = attr "hx-delete" + + /// Disables htmx processing for the given node and any children nodes + let _hxDisable = flag "hx-disable" + + /// Disinherit all ("*") or specific htmx attributes + let _hxDisinherit = attr "hx-disinherit" + + /// Changes the request encoding type + let _hxEncoding = attr "hx-encoding" + + /// Extensions to use for this element + let _hxExt = attr "hx-ext" + + /// Issues a GET to the specified URL + let _hxGet = attr "hx-get" + + /// Adds to the headers that will be submitted with the request + let _hxHeaders = attr "hx-headers" + + /// The element to snapshot and restore during history navigation + let _hxHistoryElt = flag "hx-history-elt" + + /// Includes additional data in AJAX requests + let _hxInclude = attr "hx-include" + + /// The element to put the htmx-request class on during the AJAX request + let _hxIndicator = attr "hx-indicator" + + /// Overrides a previous `hx-boost` + let _hxNoBoost = attr "hx-boost" "false" + + /// Filters the parameters that will be submitted with a request + let _hxParams = attr "hx-params" + + /// Issues a PATCH to the specified URL + let _hxPatch = attr "hx-patch" + + /// Issues a POST to the specified URL + let _hxPost = attr "hx-post" + + /// Preserves an element between requests + let _hxPreserve = attr "hx-preserve" "true" + + /// Shows a prompt before submitting a request + let _hxPrompt = attr "hx-prompt" + + /// Pushes the URL into the location bar, creating a new history entry + let _hxPushUrl = attr "hx-push-url" + + /// Issues a PUT to the specified URL + 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" + + /// 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 module Script = - /// Script tag to load the minified version from unpkg.com - let minified = - script [ - _src "https://unpkg.com/htmx.org@1.7.0" - _integrity "sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" - _crossorigin "anonymous" - ] [] + /// Script tag to load the minified version from unpkg.com + let minified = + script [ _src "https://unpkg.com/htmx.org@1.8.0" + _integrity "sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" + _crossorigin "anonymous" ] [] - /// Script tag to load the unminified version from unpkg.com - let unminified = - script [ - _src "https://unpkg.com/htmx.org@1.7.0/dist/htmx.js" - _integrity "sha384-ESk4PjE7dwjGkEciohREmmf8rLMX0E95MKwxM3bvC90sZ3XbF2TELnVk2w7bX0d9" - _crossorigin "anonymous" - ] [] + /// Script tag to load the unminified version from unpkg.com + let unminified = + script [ _src "https://unpkg.com/htmx.org@1.8.0/dist/htmx.js" + _integrity "sha384-mrsv860ohrJ5KkqRxwXXj6OIT6sONUxOd+1kvbqW351hQd7JlfFnM0tLetA76GU0" + _crossorigin "anonymous" ] []