diff --git a/src/Common/Common.fs b/src/Common/Common.fs index d4c5e79..7b13f34 100644 --- a/src/Common/Common.fs +++ b/src/Common/Common.fs @@ -11,15 +11,16 @@ let internal toJson (pairs: (string * string) list) = |> String.concat ", " |> sprintf "{ %s }" -/// Convert a boolean to lowercase true or false +/// Convert a boolean to lowercase "true" or "false" /// The boolean value to convert -/// "true" for true, "false" for false +/// "true" for true, "false" for false let internal toLowerBool (boolValue: bool) = (string boolValue).ToLowerInvariant() -/// Valid values for the hx-swap attribute / HX-Reswap header -/// May be combined with swap / settle / scroll / show config) +/// Valid values for the hx-swap attribute / HX-Reswap header +/// May be combined with swap / settle / scroll / show config) +/// Documentation [] module HxSwap = diff --git a/src/Htmx/Htmx.fs b/src/Htmx/Htmx.fs index 09c1698..affa2db 100644 --- a/src/Htmx/Htmx.fs +++ b/src/Htmx/Htmx.fs @@ -11,7 +11,7 @@ let private hdr (headers : IHeaderDictionary) hdr = /// Extensions to the header dictionary 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 @@ -19,29 +19,27 @@ type IHeaderDictionary with 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 - /// The user response to an hx-prompt + /// The user response to an 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 - /// The id attribute of the target element if it exists + /// The id attribute of the target element if it exists member this.HxTarget with get () = hdr this "HX-Target" - /// The id attribute of the triggered element if it exists + /// The id attribute of the triggered element if it exists member this.HxTrigger with get () = hdr this "HX-Trigger" - /// The name attribute of the triggered element if it exists + /// The name attribute of the triggered element if it exists member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" @@ -64,99 +62,111 @@ module Handlers = open Giraffe.Htmx.Common + /// Instruct htmx to perform a client-side redirect for content + /// The path where the content should be found + /// An HTTP handler with the HX-Location header set + /// Documentation + let withHxLocation (path: string) : HttpHandler = + setHttpHeader "HX-Location" path + /// Pushes a new url into the history stack /// The URL to be pushed - /// An HTTP handler with the HX-Push-Url header set + /// An HTTP handler with the HX-Push-Url header set + /// Use to explicitly not push a new URL + /// Documentation let withHxPushUrl (url: string) : HttpHandler = setHttpHeader "HX-Push-Url" url /// Explicitly do not push a new URL into the history stack - /// An HTTP handler with the HX-Push-Url header set to false + /// An HTTP handler with the HX-Push-Url header set to false + /// Documentation let withHxNoPushUrl : HttpHandler = toLowerBool false |> withHxPushUrl - /// Pushes a new url into the history stack - [] - let withHxPush = withHxPushUrl - - /// 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 /// The URL to which the client should be redirected - /// An HTTP handler with the HX-Redirect header set + /// An HTTP handler with the HX-Redirect header set + /// Documentation let withHxRedirect (url: string) : HttpHandler = setHttpHeader "HX-Redirect" url - /// If set to true the client side will do a full refresh of the page + /// If set to true the client side will do a full refresh of the page /// Whether the client should refresh their page - /// An HTTP handler with the HX-Refresh header set + /// An HTTP handler with the HX-Refresh header set let withHxRefresh shouldRefresh : HttpHandler = (toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh /// Replaces the current URL in the history stack /// The URL to place in the history stack in place of the current one - /// An HTTP handler with the HX-Replace-URL header set + /// An HTTP handler with the HX-Replace-URL header set + /// Use to explicitly not replace the current URL + /// Documentation let withHxReplaceUrl url : HttpHandler = setHttpHeader "HX-Replace-Url" url /// Explicitly do not replace the current URL in the history stack - /// An HTTP handler with the HX-Replace-URL header set to false + /// An HTTP handler with the HX-Replace-URL header set to false + /// Documentation let withHxNoReplaceUrl : HttpHandler = toLowerBool false |> withHxReplaceUrl /// Override which portion of the response will be swapped into the target document /// The selector for the new response target - /// An HTTP handler with the HX-Reselect header set + /// An HTTP handler with the HX-Reselect header set let withHxReselect (target: string) : HttpHandler = setHttpHeader "HX-Reselect" target - /// Override the hx-swap attribute from the initiating element + /// Override the hx-swap attribute from the initiating element /// The swap value to override - /// An HTTP handler with the HX-Reswap header set + /// An HTTP handler with the HX-Reswap header set /// Use HxSwap constants for best results let withHxReswap (swap: string) : HttpHandler = setHttpHeader "HX-Reswap" swap - /// Allows you to override the hx-target attribute + /// Allows you to override the hx-target attribute /// The new target for the response - /// An HTTP handler with the HX-Retarget header set + /// An HTTP handler with the HX-Retarget header set let withHxRetarget (target: string) : HttpHandler = setHttpHeader "HX-Retarget" target /// Allows you to trigger a single client side event /// The call to the event that should be triggered - /// An HTTP handler with the HX-Trigger header set + /// An HTTP handler with the HX-Trigger header set + /// Documentation let withHxTrigger (evt: string) : HttpHandler = setHttpHeader "HX-Trigger" evt /// Allows you to trigger multiple client side events /// The calls to events that should be triggered - /// An HTTP handler with the HX-Trigger header set for all given events + /// An HTTP handler with the HX-Trigger header set for all given events + /// Documentation let withHxTriggerMany evts : HttpHandler = toJson evts |> setHttpHeader "HX-Trigger" /// Allows you to trigger a single client side event after changes have settled /// The call to the event that should be triggered - /// An HTTP handler with the HX-Trigger-After-Settle header set + /// An HTTP handler with the HX-Trigger-After-Settle header set + /// Documentation let withHxTriggerAfterSettle (evt: string) : HttpHandler = setHttpHeader "HX-Trigger-After-Settle" evt /// Allows you to trigger multiple client side events after changes have settled /// The calls to events that should be triggered - /// An HTTP handler with the HX-Trigger-After-Settle header set for all given events + /// An HTTP handler with the HX-Trigger-After-Settle header set for all given events + /// Documentation let withHxTriggerManyAfterSettle evts : HttpHandler = toJson evts |> setHttpHeader "HX-Trigger-After-Settle" /// Allows you to trigger a single client side event after DOM swapping occurs /// The call to the event that should be triggered - /// An HTTP handler with the HX-Trigger-After-Swap header set + /// An HTTP handler with the HX-Trigger-After-Swap header set + /// Documentation let withHxTriggerAfterSwap (evt: string) : HttpHandler = setHttpHeader "HX-Trigger-After-Swap" evt /// Allows you to trigger multiple client side events after DOM swapping occurs /// The calls to events that should be triggered - /// An HTTP handler with the HX-Trigger-After-Swap header set for all given events + /// An HTTP handler with the HX-Trigger-After-Swap header set for all given events + /// Documentation let withHxTriggerManyAfterSwap evts : HttpHandler = toJson evts |> setHttpHeader "HX-Trigger-After-Swap" diff --git a/src/Tests/Htmx.fs b/src/Tests/Htmx.fs index 5b1d162..76121e1 100644 --- a/src/Tests/Htmx.fs +++ b/src/Tests/Htmx.fs @@ -11,22 +11,22 @@ let dictExtensions = testList "IHeaderDictionaryExtensions" [ testList "HxBoosted" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned" } test "succeeds when the header is present and true" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Boosted", "true") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Boosted", "true") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true" } test "succeeds when the header is present and false" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Boosted", "false") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Boosted", "false") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false" @@ -34,14 +34,14 @@ let dictExtensions = ] testList "HxCurrentUrl" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned" } test "succeeds when the header is present" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Current-URL", "http://localhost/test.htm") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Current-URL", "http://localhost/test.htm") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present" Expect.equal @@ -51,22 +51,22 @@ let dictExtensions = ] testList "HxHistoryRestoreRequest" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned" } test "succeeds when the header is present and true" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-History-Restore-Request", "true") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-History-Restore-Request", "true") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true" } test "succeeds when the header is present and false" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-History-Restore-Request", "false") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-History-Restore-Request", "false") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" Expect.isFalse @@ -75,14 +75,14 @@ let dictExtensions = ] testList "HxPrompt" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned" } test "succeeds when the header is present" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Prompt", "of course") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Prompt", "of course") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present" Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect" @@ -90,22 +90,22 @@ let dictExtensions = ] testList "HxRequest" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned" } test "succeeds when the header is present and true" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Request", "true") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true" } test "succeeds when the header is present and false" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "false") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Request", "false") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false" @@ -113,14 +113,14 @@ let dictExtensions = ] testList "HxTarget" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned" } test "succeeds when the header is present" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Target", "#leItem") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Target", "#leItem") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present" Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect" @@ -128,14 +128,14 @@ let dictExtensions = ] testList "HxTrigger" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () + let ctx = Substitute.For() ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned" } test "succeeds when the header is present" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Trigger", "#trig") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Trigger", "#trig") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present" Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect" @@ -143,14 +143,14 @@ let dictExtensions = ] testList "HxTriggerName" [ test "succeeds when the header is not present" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned" } test "HxTriggerName succeeds when the header is present" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Trigger-Name", "click") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Trigger-Name", "click") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present" Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect" @@ -163,36 +163,36 @@ let reqExtensions = testList "HttpRequestExtensions" [ testList "IsHtmx" [ test "succeeds when request is not from htmx" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request" } test "succeeds when request is from htmx" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Request", "true") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request" } ] testList "IsHtmxRefresh" [ test "succeeds when request is not from htmx" { - let ctx = Substitute.For () - ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore + let ctx = Substitute.For() + ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" } test "succeeds when request is from htmx, but not a refresh" { - let ctx = Substitute.For () - let dic = HeaderDictionary () - dic.Add ("HX-Request", "true") + let ctx = Substitute.For() + let dic = HeaderDictionary() + dic.Add("HX-Request", "true") ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" } test "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") + 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 Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh" } @@ -202,30 +202,38 @@ let reqExtensions = open System.Threading.Tasks /// 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) /// Tests for the HttpHandler functions provided in the Handlers module let handlers = testList "HandlerTests" [ + testTask "withHxLocation succeeds" { + let ctx = Substitute.For() + let dic = HeaderDictionary() + ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore + let! _ = withHxLocation "/pagina-otro.html" next ctx + Expect.isTrue (dic.ContainsKey "HX-Location") "The HX-Location header should be present" + Expect.equal dic["HX-Location"].[0] "/pagina-otro.html" "The HX-Location value was incorrect" + } testTask "withHxPushUrl succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxPushUrl "/a-new-url" next ctx Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect" } testTask "withHxNoPushUrl succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxNoPushUrl next ctx Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect" } testTask "withHxRedirect succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxRedirect "/somewhere-else" next ctx Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present" @@ -233,16 +241,16 @@ let handlers = } testList "withHxRefresh" [ testTask "succeeds when set to true" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxRefresh true next ctx Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect" } testTask "succeeds when set to false" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxRefresh false next ctx Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" @@ -250,56 +258,56 @@ let handlers = } ] testTask "withHxReplaceUrl succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxReplaceUrl "/a-substitute-url" next ctx Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect" } testTask "withHxNoReplaceUrl succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxNoReplaceUrl next ctx Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect" } testTask "withHxReselect succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxReselect "#test" next ctx Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present" Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect" } testTask "withHxReswap succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxReswap HxSwap.BeforeEnd next ctx Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present" Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect" } testTask "withHxRetarget succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxRetarget "#somewhereElse" next ctx Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present" Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect" } testTask "withHxTrigger succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTrigger "doSomething" next ctx Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect" } testTask "withHxTriggerMany succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" @@ -307,8 +315,8 @@ let handlers = dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect" } testTask "withHxTriggerAfterSettle succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTriggerAfterSettle "byTheWay" next ctx Expect.isTrue @@ -316,8 +324,8 @@ let handlers = Expect.equal dic["HX-Trigger-After-Settle"].[0] "byTheWay" "The HX-Trigger-After-Settle value was incorrect" } testTask "withHxTriggerManyAfterSettle succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx Expect.isTrue @@ -327,16 +335,16 @@ let handlers = "The HX-Trigger-After-Settle value was incorrect" } testTask "withHxTriggerAfterSwap succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTriggerAfterSwap "justASec" next ctx Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present" Expect.equal dic["HX-Trigger-After-Swap"].[0] "justASec" "The HX-Trigger-After-Swap value was incorrect" } testTask "withHxTriggerManyAfterSwap succeeds" { - let ctx = Substitute.For () - let dic = HeaderDictionary () + let ctx = Substitute.For() + let dic = HeaderDictionary() ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present" diff --git a/src/Tests/ViewEngine.fs b/src/Tests/ViewEngine.fs index 4653bbe..779b194 100644 --- a/src/Tests/ViewEngine.fs +++ b/src/Tests/ViewEngine.fs @@ -406,8 +406,8 @@ let hxEvent = Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct" } ] - ] + /// Tests for the HxHeaders module let hxHeaders = testList "HxHeaders" [ @@ -726,7 +726,7 @@ let attributes = |> shouldRender """
""" } test "_hxHistory succeeds" { - span [ _hxHistory "false" ] [] |> shouldRender """""" + span [ _hxHistory false ] [] |> shouldRender """""" } test "_hxHistoryElt succeeds" { table [ _hxHistoryElt ] [] |> shouldRender """
""" @@ -839,7 +839,7 @@ let renderFragment = /// Validate that the two object references are the same object let isSame obj1 obj2 message = - Expect.isTrue (obj.ReferenceEquals (obj1, obj2)) message + Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message testList "findIdNode" [ test "fails with a Text node" { @@ -921,7 +921,7 @@ let renderFragment = } test "fails when an ID is not matched" { Expect.equal - (RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes (nodeNotFound "whiff")) + (RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes(nodeNotFound "whiff")) "HTML bytes are incorrect" } ] @@ -938,7 +938,7 @@ let renderFragment = } test "fails when an ID is not matched" { Expect.equal - (RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes (nodeNotFound "foo")) + (RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes(nodeNotFound "foo")) "HTML bytes are incorrect" } ] @@ -946,31 +946,31 @@ let renderFragment = testList "IntoStringBuilder" [ testList "htmlFromNodes" [ test "succeeds when an ID is matched" { - let sb = StringBuilder () + let sb = StringBuilder() RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me" [ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]] Expect.equal (string sb) """;)""" "HTML is incorrect" } test "fails when an ID is not matched" { - let sb = StringBuilder () + let sb = StringBuilder() RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" [] Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect" } ] testList "htmlFromNode" [ test "succeeds when ID is matched at top level" { - let sb = StringBuilder () + let sb = StringBuilder() RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ]) Expect.equal (string sb) """

pinnacle

""" "HTML is incorrect" } test "succeeds when ID is matched in child element" { - let sb = StringBuilder () + let sb = StringBuilder() div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]] |> RenderFragment.IntoStringBuilder.htmlFromNode sb "it" Expect.equal (string sb) """

is here

""" "HTML is incorrect" } test "fails when an ID is not matched" { - let sb = StringBuilder () + let sb = StringBuilder() RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr []) Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect" } diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index d8784c9..5d69ca1 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -1,7 +1,7 @@ /// Types and functions supporting htmx attributes in Giraffe View Engine module Giraffe.ViewEngine.Htmx -/// Valid values for the hx-encoding attribute +/// Valid values for the hx-encoding attribute [] module HxEncoding = @@ -195,11 +195,11 @@ type HxEvent = /// The htmx event name override this.ToString() = fst HxEvent.Values[this] - /// The hx-on variant of the htmx event name + /// The hx-on variant of the htmx event name member this.ToHxOnString() = snd HxEvent.Values[this] -/// Helper to create the hx-headers attribute +/// Helper to create the hx-headers attribute [] module HxHeaders = @@ -207,7 +207,8 @@ module HxHeaders = let From = Giraffe.Htmx.Common.toJson -/// Values / helpers for the hx-params attribute +/// Values / helpers for the hx-params attribute +/// Documentation [] module HxParams = @@ -219,18 +220,19 @@ module HxParams = /// Include the specified parameters /// One or more fields to include in the request - /// The list of fields for the hx-params attribute value + /// The list of fields for the hx-params attribute value let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") /// Exclude the specified parameters /// One or more fields to exclude from the request - /// The list of fields for the hx-params attribute value prefixed with "not" + /// The list of fields for the hx-params attribute value prefixed with "not" let Except fields = With fields |> sprintf "not %s" -/// Helpers to define hx-request attribute values +/// Helpers to define hx-request attribute values +/// Documentation [] module HxRequest = @@ -251,21 +253,21 @@ module HxRequest = $"\"timeout\": {ms}" /// Include or exclude credentials from the request - /// true if credentials should be sent, false if not + /// true if credentials should be sent, false if not /// A string with the configured credential options let Credentials send = (toLowerBool >> sprintf "\"credentials\": %s") send /// Exclude or include headers from the request /// - /// true if no headers should be sent; false if headers should be sent + /// true if no headers should be sent; false if headers should be sent /// /// A string with the configured header options let NoHeaders exclude = (toLowerBool >> sprintf "\"noHeaders\": %s") exclude -/// Helpers for the `hx-trigger` attribute +/// Helpers for the hx-trigger attribute [] module HxTrigger = @@ -366,7 +368,8 @@ module HxTrigger = let QueueNone = Queue "none" -/// Helper to create the `hx-vals` attribute +/// Helper to create the hx-vals attribute +/// Documentation [] module HxVals = @@ -374,149 +377,291 @@ module HxVals = let From = Giraffe.Htmx.Common.toJson +open Giraffe.Htmx + /// Attributes and flags for htmx [] module HtmxAttrs = /// Progressively enhances anchors and forms to use AJAX requests - /// Use _hxNoBoost to set to false + /// Use _hxNoBoost to set to false + /// Documentation let _hxBoost = attr "hx-boost" "true" - /// Shows a confirm() dialog before issuing a request + /// Shows a confirm() dialog before issuing a request /// The prompt to present to the user when seeking their confirmation - /// A configured hx-confirm attribute + /// A configured hx-confirm attribute + /// Documentation let _hxConfirm prompt = attr "hx-confirm" prompt - /// Issues a DELETE to the specified URL - /// The URL to which the DELETE request should be sent - /// A configured hx-delete attribute + /// Issues a DELETE to the specified URL + /// The URL to which the DELETE request should be sent + /// A configured hx-delete attribute + /// Documentation let _hxDelete url = attr "hx-delete" url /// Disables htmx processing for the given node and any children nodes + /// Documentation let _hxDisable = flag "hx-disable" /// Specifies elements that should be disabled when an htmx request is in flight /// The element to disable when an htmx request is in flight - /// A configured hx-disabled-elt attribute + /// A configured hx-disabled-elt attribute + /// Documentation let _hxDisabledElt elt = attr "hx-disabled-elt" elt /// Disinherit all ("*") or specific htmx attributes /// The htmx attributes to disinherit (should start with "hx-") - /// A configured hx-disinherit attribute + /// A configured hx-disinherit attribute + /// Documentation let _hxDisinherit hxAttrs = attr "hx-disinherit" hxAttrs /// Changes the request encoding type - /// The encoding type (use HxEncoding constants) - /// A configured hx-encoding attribute + /// The encoding type (use HxEncoding constants) + /// A configured hx-encoding attribute + /// + /// Documentation let _hxEncoding enc = attr "hx-encoding" enc /// Extensions to use for this element /// A list of extensions to apply to this element - /// A configured hx-ext attribute + /// A configured hx-ext attribute + /// Documentation let _hxExt exts = attr "hx-ext" exts - /// Issues a GET to the specified URL - /// The URL to which the GET request should be sent - /// A configured hx-get attribute + /// Issues a GET to the specified URL + /// The URL to which the GET request should be sent + /// A configured hx-get attribute + /// Documentation let _hxGet url = attr "hx-get" url /// Adds to the headers that will be submitted with the request - [] - let _hxHeaders = attr "hx-headers" + /// The headers to include with the request + /// A configured hx-headers attribute + /// Documentation + let _hxHeaders hdrs = + attr "hx-headers" hdrs + /// /// Set to "false" to prevent pages with sensitive information from being stored in the history cache - let _hxHistory = attr "hx-history" + /// + /// Whether the page should be stored in the history cache + /// A configured hx-history attribute + /// Documentation + let _hxHistory shouldStore = + attr "hx-history" (toLowerBool shouldStore) - /// The element to snapshot and restore during history navigation - let _hxHistoryElt = flag "hx-history-elt" + /// The element to snapshot and restore during history navigation + /// Documentation + let _hxHistoryElt = + flag "hx-history-elt" - /// Includes additional data in AJAX requests - let _hxInclude = attr "hx-include" + /// Includes additional data in AJAX requests + /// The specification of what should be included in the request + /// A configured hx-include attribute + /// Documentation + let _hxInclude spec = + attr "hx-include" spec - /// The element to put the htmx-request class on during the AJAX request - let _hxIndicator = attr "hx-indicator" + /// The element to put the htmx-request class on during the AJAX request + /// The selector for the indicator element + /// A configured hx-indicator attribute + /// Documentation + let _hxIndicator selector = + attr "hx-indicator" selector - /// Overrides a previous `hx-boost` - let _hxNoBoost = attr "hx-boost" "false" + /// Overrides a previous hx-boost (hx-boost="false") + /// Documentation + let _hxNoBoost = + attr "hx-boost" "false" - /// Attach an event handler for DOM events - let _hxOnEvent evtName = - attr $"hx-on:%s{evtName}" + /// Attach an event handler for DOM events + /// The name of the event + /// The script to be executed when the event occurs + /// A configured hx-on attribute + /// Documentation + let _hxOnEvent evtName handler = + attr $"hx-on:%s{evtName}" handler - /// Attach an event handler for htmx events - let _hxOnHxEvent (hxEvent: HxEvent) = - _hxOnEvent $":{hxEvent.ToHxOnString()}" + /// Attach an event handler for htmx events + /// The HxEvent to be handled + /// The script to be executed when the event occurs + /// A configured hx-on:: attribute + /// + /// Documentation + let _hxOnHxEvent (hxEvent: HxEvent) handler = + _hxOnEvent $":{hxEvent.ToHxOnString()}" handler - /// Filters the parameters that will be submitted with a request - let _hxParams = attr "hx-params" + /// Filters the parameters that will be submitted with a request + /// The fields to include (use HxParams to generate this value) + /// A configured hx-params attribute + /// + /// Documentation + let _hxParams toInclude = + attr "hx-params" toInclude - /// Issues a PATCH to the specified URL - let _hxPatch = attr "hx-patch" + /// Issues a PATCH to the specified URL + /// The URL to which the request should be directed + /// A configured hx-patch attribute + /// Documentation + let _hxPatch url = + attr "hx-patch" url - /// Issues a POST to the specified URL - let _hxPost = attr "hx-post" + /// Issues a POST to the specified URL + /// The URL to which the request should be directed + /// A configured hx-post attribute + /// Documentation + let _hxPost url = + attr "hx-post" url - /// Preserves an element between requests - let _hxPreserve = attr "hx-preserve" "true" + /// Preserves an element between requests + /// Documentation + let _hxPreserve = + flag "hx-preserve" - /// Shows a prompt before submitting a request - let _hxPrompt = attr "hx-prompt" + /// Shows a prompt() dialog before submitting a request + /// The text for the prompt + /// A configured hx-prompt attribute + /// The value provided will be in the HX-Prompt request header + /// Documentation + let _hxPrompt text = + attr "hx-prompt" text - /// Pushes the URL into the location bar, creating a new history entry - let _hxPushUrl = attr "hx-push-url" + /// Pushes the URL into the location bar, creating a new history entry + /// + ///
    + ///
  • "true" to push the fetched URL
  • + ///
  • "false" to explicitly not push the fetched URL
  • + ///
  • A specific URL to push
  • + ///
+ /// + /// A configured hx-push-url attribute + /// Documentation + let _hxPushUrl spec = + attr "hx-push-url" spec - /// Issues a PUT to the specified URL - let _hxPut = attr "hx-put" + /// Issues a PUT to the specified URL + /// The URL to which the request should be directed + /// A configured hx-put attribute + /// Documentation + let _hxPut url = + attr "hx-put" url - /// Replaces the current URL in the browser's history stack - let _hxReplaceUrl = attr "hx-replace-url" + /// Replaces the current URL in the browser's history stack + /// + ///
    + ///
  • "true" to replace the current URL with the fetched one
  • + ///
  • "false" to explicitly replace nothing
  • + ///
  • A specific URL to replace in the browser's history
  • + ///
+ /// + /// A configured hx-replace-url attribute + /// Documentation + let _hxReplaceUrl spec = + attr "hx-replace-url" spec - /// Configures various aspects of the request - let _hxRequest = attr "hx-request" + /// Configures various aspects of the request + /// The configuration spec (use HxRequest.Configure to create value) + /// A configured hx-request attribute + /// + /// Documentation + let _hxRequest spec = + attr "hx-request" spec - /// Selects a subset of the server response to process - let _hxSelect = attr "hx-select" + /// Selects a subset of the server response to process + /// A CSS selector for the content to be selected + /// A configured hx-select attribute + /// Documentation + let _hxSelect selector = + attr "hx-select" selector - /// Selects a subset of an out-of-band server response - let _hxSelectOob = attr "hx-select-oob" + /// Selects a subset of an out-of-band server response + /// One or more comma-delimited CSS selectors for the content to be selected + /// A configured hx-select-oob attribute + /// Documentation + let _hxSelectOob selectors = + attr "hx-select-oob" selectors - /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd') - let _hxSwap = attr "hx-swap" + /// + /// Controls how the response content is swapped into the DOM (e.g. outerHTML or beforeEnd) + /// + /// The type of swap to perform (use HxSwap values) + /// A configured hx-swap attribute + /// Documentation + let _hxSwap swap = + attr "hx-swap" swap - /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd'), enabling CSS - /// transitions - let _hxSwapWithTransition = sprintf "%s transition:true" >> _hxSwap + /// + /// Controls how the response content is swapped into the DOM (e.g. outerHTML or beforeEnd), enabling + /// CSS transitions + /// + /// The type of swap to perform (use HxSwap values) + /// A configured hx-swap attribute + /// Documentation + let _hxSwapWithTransition swap = + _hxSwap $"%s{swap} transition:true" + /// /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target - let _hxSwapOob = attr "hx-swap-oob" + /// + /// + ///
    + ///
  • "true" to mark this as an OOB swap
  • + ///
  • Any HxSwap value
  • + ///
  • Any HxSwap value, followed by a colon (:) and a CSS selector
  • + ///
+ /// + /// A configured hx-swap-oob attribute + /// Documentation + let _hxSwapOob swap = + attr "hx-swap-oob" swap /// 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 target element to be swapped + /// A CSS selector or relative reference (or both) to identify the target + /// A configured hx-target attribute + /// Documentation + let _hxTarget selector = + attr "hx-target" selector /// 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" + /// Validate an input element (uses HTML5 validation API) + /// Documentation + let _hxValidate = + flag "hx-validate" - /// Adds to the parameters that will be submitted with the request - let _hxVals = attr "hx-vals" + /// Adds to the parameters that will be submitted with the request + /// The values for the parameters (use HxVals.From to create) + /// A configured hx-vals attribute + /// + /// Documentation + let _hxVals values = + attr "hx-vals" values - /// The name of the message to swap into the DOM. - let _sseSwap = attr "sse-swap" + /// The URL of the SSE server + /// The URL from which events will be received + /// A configured sse-connect attribute + /// Extension Docs + let _sseConnect url = + attr "sse-connect" url - /// The URL of the SSE server. - let _sseConnect = attr "sse-connect" + /// The name(s) of the message(s) to swap into the DOM + /// The message names (comma-delimited) to swap (use "message" for unnamed events) + /// A configured sse-swap attribute + /// Extension Docs + let _sseSwap messages = + attr "sse-swap" messages /// Script tags to pull htmx into a web page @@ -552,9 +697,9 @@ module RenderFragment = $"– ID {nodeId} not found –" /// Find the node with the named ID - /// The id attribute to find + /// The id attribute to find /// The node tree to search - /// The node with the requested id attribute, or None if it was not found + /// The node with the requested id attribute, or None if it was not found let rec findIdNode nodeId (node: XmlNode) : XmlNode option = match node with | Text _ -> None @@ -567,18 +712,18 @@ module RenderFragment = module AsString = /// Render to HTML for the given ID - /// The id attribute for the node to be rendered + /// The id attribute for the node to be rendered /// The node trees to search - /// The HTML for the given id node, or an error message if it was not found + /// The HTML for the given id node, or an error message if it was not found let htmlFromNodes nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.AsString.htmlNode idNode | None -> nodeNotFound nodeId /// Render to HTML for the given ID - /// The id attribute for the node to be rendered + /// The id attribute for the node to be rendered /// The node tree to search - /// The HTML for the given id node, or an error message if it was not found + /// The HTML for the given id node, or an error message if it was not found let htmlFromNode nodeId node = match findIdNode nodeId node with | Some idNode -> RenderView.AsString.htmlNode idNode @@ -591,18 +736,18 @@ module RenderFragment = let private utf8 = System.Text.Encoding.UTF8 /// Render to bytes for the given ID - /// The id attribute for the node to be rendered + /// The id attribute for the node to be rendered /// The node trees to search - /// The bytes for the given id node, or an error message if it was not found + /// The bytes for the given id node, or an error message if it was not found let htmlFromNodes nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.AsBytes.htmlNode idNode | None -> nodeNotFound nodeId |> utf8.GetBytes /// Render to bytes for the given ID - /// The id attribute for the node to be rendered + /// The id attribute for the node to be rendered /// The node tree to search - /// The bytes for the given id node, or an error message if it was not found + /// The bytes for the given id node, or an error message if it was not found let htmlFromNode nodeId node = match findIdNode nodeId node with | Some idNode -> RenderView.AsBytes.htmlNode idNode @@ -612,18 +757,18 @@ module RenderFragment = [] module IntoStringBuilder = - /// Render HTML into a StringBuilder for the given ID - /// The StringBuilder into which the bytes will be rendered - /// The id attribute for the node to be rendered + /// Render HTML into a StringBuilder for the given ID + /// The StringBuilder into which the bytes will be rendered + /// The id attribute for the node to be rendered /// The node trees to search let htmlFromNodes sb nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode | None -> nodeNotFound nodeId |> sb.Append |> ignore - /// Render HTML into a StringBuilder for the given ID - /// The StringBuilder into which the bytes will be rendered - /// The id attribute for the node to be rendered + /// Render HTML into a StringBuilder for the given ID + /// The StringBuilder into which the bytes will be rendered + /// The id attribute for the node to be rendered /// The node tree to search let htmlFromNode sb nodeId node = match findIdNode nodeId node with