From 6b7458070b00ac36e9fc44da7a969f33780efe31 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 3 Jul 2025 00:15:24 +0000 Subject: [PATCH] v2.0.6 (#15) - Adds XML documentation (#13) - Adds `HxSync` module and attribute helper - Updates script version to 2.0.6 - Drops .NET 6 support (#14 ) Reviewed-on: https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/pulls/15 --- src/Common/Common.fs | 43 +- src/Common/Giraffe.Htmx.Common.fsproj | 7 +- src/Common/README.md | 2 +- src/Directory.Build.props | 13 +- src/Htmx/Giraffe.Htmx.fsproj | 1 + src/Htmx/Htmx.fs | 193 ++-- src/Htmx/README.md | 2 +- src/Tests/Htmx.fs | 198 +++-- src/Tests/ViewEngine.fs | 59 +- .../Giraffe.ViewEngine.Htmx.fsproj | 1 + src/ViewEngine.Htmx/Htmx.fs | 832 +++++++++++++----- src/ViewEngine.Htmx/README.md | 4 +- 12 files changed, 920 insertions(+), 435 deletions(-) diff --git a/src/Common/Common.fs b/src/Common/Common.fs index 4c523c1..85b1219 100644 --- a/src/Common/Common.fs +++ b/src/Common/Common.fs @@ -1,28 +1,53 @@ -/// Common definitions shared between attribute values and response headers +/// 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) +/// Serialize a list of key/value pairs to JSON (very rudimentary) +/// The key/value pairs to be serialized to JSON +/// A string with the key/value pairs serialized to JSON +let internal toJson (pairs: (string * string) list) = + pairs + |> List.map (fun pair -> sprintf "\"%s\": \"%s\"" (fst pair) ((snd pair).Replace ("\"", "\\\""))) + |> String.concat ", " + |> sprintf "{ %s }" + +/// Convert a boolean to lowercase "true" or "false" +/// The boolean value to convert +/// "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) +/// Documentation [] module HxSwap = - /// The default, replace the inner html of the target element + /// The default, replace the inner HTML of the target element + [] let InnerHtml = "innerHTML" - /// Replace the entire target element with the response + /// Replace the entire target element with the response + [] let OuterHtml = "outerHTML" - /// Insert the response before the target element + /// Insert the response before the target element + [] let BeforeBegin = "beforebegin" - /// Insert the response before the first child of the target element + /// 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 + /// Insert the response after the last child of the target element + [] let BeforeEnd = "beforeend" - /// Insert the response after the target element + /// Insert the response after the target element + [] let AfterEnd = "afterend" - /// Does not append content from response (out of band items will still be processed). + /// 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 index 1aa7bbd..465b707 100644 --- a/src/Common/Giraffe.Htmx.Common.fsproj +++ b/src/Common/Giraffe.Htmx.Common.fsproj @@ -4,6 +4,7 @@ true Common definitions for Giraffe.Htmx README.md + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml @@ -15,5 +16,9 @@ - + + + + + diff --git a/src/Common/README.md b/src/Common/README.md index 6747422..1b9f74a 100644 --- a/src/Common/README.md +++ b/src/Common/README.md @@ -2,4 +2,4 @@ This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. -**htmx version: 2.0.4** +**htmx version: 2.0.6** diff --git a/src/Directory.Build.props b/src/Directory.Build.props index c622ac0..58015d0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,9 +1,16 @@  - net6.0;net8.0;net9.0 - 2.0.4 - Update script tags to pull htmx 2.0.4 (no header or attribute changes) + net8.0;net9.0 + 2.0.6 + true + - All packages now have full XML documentation +- Adds HxSync module and attribute helper to view engine +- Updates script tags to pull htmx 2.0.6 (no header or attribute changes) +- Drops .NET 6 support + +NOTE: The CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed domains accordingly + danieljsummers Bit Badger Solutions https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx diff --git a/src/Htmx/Giraffe.Htmx.fsproj b/src/Htmx/Giraffe.Htmx.fsproj index 4111b40..be3f9bf 100644 --- a/src/Htmx/Giraffe.Htmx.fsproj +++ b/src/Htmx/Giraffe.Htmx.fsproj @@ -4,6 +4,7 @@ true htmx header extensions and helpers for Giraffe README.md + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml diff --git a/src/Htmx/Htmx.fs b/src/Htmx/Htmx.fs index b21559a..affa2db 100644 --- a/src/Htmx/Htmx.fs +++ b/src/Htmx/Htmx.fs @@ -11,121 +11,162 @@ 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` - member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse + /// 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 + /// 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 + /// 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" + /// 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 attribute 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 attribute 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 attribute 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 +/// HTTP handlers for setting output headers [] module Handlers = - /// Convert a boolean to lowercase `true` or `false` - let private toLowerBool (trueOrFalse : bool) = - (string trueOrFalse).ToLowerInvariant () + 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 + /// Use to explicitly not push a new URL + /// Documentation + let withHxPushUrl (url: string) : HttpHandler = + setHttpHeader "HX-Push-Url" url - /// 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 withHxPushUrl : string -> HttpHandler = - setHttpHeader "HX-Push-Url" - - /// Explicitly do not push a new URL into the history stack + /// Explicitly do not push a new URL into the history stack + /// 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 - let withHxRedirect : string -> HttpHandler = - setHttpHeader "HX-Redirect" + /// 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 + /// Documentation + let withHxRedirect (url: string) : HttpHandler = + setHttpHeader "HX-Redirect" url - /// 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 full refresh of the page + /// Whether the client should refresh their page + /// 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 - let withHxReplaceUrl : string -> HttpHandler = - setHttpHeader "HX-Replace-Url" + /// 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 + /// 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 + /// Explicitly do not replace the current URL in the history stack + /// 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 - let withHxReselect : string -> HttpHandler = - setHttpHeader "HX-Reselect" + /// 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 + let withHxReselect (target: string) : HttpHandler = + setHttpHeader "HX-Reselect" target - /// Override the `hx-swap` attribute from the initiating element - let withHxReswap : string -> HttpHandler = - setHttpHeader "HX-Reswap" + /// Override the hx-swap attribute from the initiating element + /// The swap value to override + /// 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 - let withHxRetarget : string -> HttpHandler = - setHttpHeader "HX-Retarget" + /// Allows you to override the hx-target attribute + /// The new target for the response + /// 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 - let withHxTrigger : string -> HttpHandler = - setHttpHeader "HX-Trigger" + /// 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 + /// Documentation + let withHxTrigger (evt: string) : HttpHandler = + setHttpHeader "HX-Trigger" evt - /// Allows you to trigger multiple client side events + /// 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 + /// Documentation let withHxTriggerMany evts : HttpHandler = toJson evts |> setHttpHeader "HX-Trigger" - /// Allows you to trigger a single client side event after changes have settled - let withHxTriggerAfterSettle : string -> HttpHandler = - setHttpHeader "HX-Trigger-After-Settle" + /// Allows you to trigger 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 + /// Documentation + let withHxTriggerAfterSettle (evt: string) : HttpHandler = + setHttpHeader "HX-Trigger-After-Settle" evt - /// Allows you to trigger multiple client side events after changes have settled + /// 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 + /// 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 - let withHxTriggerAfterSwap : string -> HttpHandler = - setHttpHeader "HX-Trigger-After-Swap" + /// 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 + /// Documentation + let withHxTriggerAfterSwap (evt: string) : HttpHandler = + setHttpHeader "HX-Trigger-After-Swap" evt - /// Allows you to trigger multiple client side events after DOM swapping occurs + /// 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 + /// Documentation let withHxTriggerManyAfterSwap evts : HttpHandler = toJson evts |> setHttpHeader "HX-Trigger-After-Swap" diff --git a/src/Htmx/README.md b/src/Htmx/README.md index 29addf7..a041166 100644 --- a/src/Htmx/README.md +++ b/src/Htmx/README.md @@ -2,7 +2,7 @@ This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`. -**htmx version: 2.0.4** +**htmx version: 2.0.6** _Upgrading from v1.x: the [migration guide](https://htmx.org/migration-guide-htmx-1/) does not currently specify any request or response header changes. This means that there are no required code changes in moving from v1.* to v2.*._ 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..f0c9cd7 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" [ @@ -497,6 +497,32 @@ let hxRequest = ] ] +/// Tests for the HxSync module +let hxSync = + testList "HxSync" [ + test "Drop is correct" { + Expect.equal HxSync.Drop "drop" "Drop is incorrect" + } + test "Abort is correct" { + Expect.equal HxSync.Abort "abort" "Abort is incorrect" + } + test "Replace is correct" { + Expect.equal HxSync.Replace "replace" "Replace is incorrect" + } + test "Queue is correct" { + Expect.equal HxSync.Queue "queue" "Queue is incorrect" + } + test "QueueFirst is correct" { + Expect.equal HxSync.QueueFirst "queue first" "QueueFirst is incorrect" + } + test "QueueLast is correct" { + Expect.equal HxSync.QueueLast "queue last" "QueueLast is incorrect" + } + test "QueueAll is correct" { + Expect.equal HxSync.QueueAll "queue all" "QueueAll is incorrect" + } + ] + /// Tests for the HxTrigger module let hxTrigger = testList "HxTrigger" [ @@ -636,6 +662,9 @@ let hxTrigger = test "succeeds when it is not the first modifier" { Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" } + test "succeeds when no type of queueing is given" { + Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect" + } ] testList "QueueFirst" [ test "succeeds when it is the first modifier" { @@ -726,7 +755,7 @@ let attributes = |> shouldRender """
""" } test "_hxHistory succeeds" { - span [ _hxHistory "false" ] [] |> shouldRender """""" + span [ _hxHistory false ] [] |> shouldRender """""" } test "_hxHistoryElt succeeds" { table [ _hxHistoryElt ] [] |> shouldRender """
""" @@ -757,7 +786,7 @@ let attributes = hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
""" } test "_hxPreserve succeeds" { - img [ _hxPreserve ] |> shouldRender """""" + img [ _hxPreserve ] |> shouldRender """""" } test "_hxPrompt succeeds" { strong [ _hxPrompt "Who goes there?" ] [] @@ -792,7 +821,8 @@ let attributes = li [ _hxSwapOob "true" ] [] |> shouldRender """
  • """ } test "_hxSync succeeds" { - nav [ _hxSync "closest form:abort" ] [] |> shouldRender """""" + nav [ _hxSync "closest form" HxSync.Abort ] [] + |> shouldRender """""" } test "_hxTarget succeeds" { header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
    """ @@ -819,14 +849,14 @@ let script = let html = RenderView.AsString.htmlNode Script.minified Expect.equal html - """""" + """""" "Minified script tag is incorrect" } test "unminified succeeds" { let html = RenderView.AsString.htmlNode Script.unminified Expect.equal html - """""" + """""" "Unminified script tag is incorrect" } ] @@ -839,7 +869,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 +951,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 +968,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 +976,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" } @@ -986,6 +1016,7 @@ let allTests = hxHeaders hxParams hxRequest + hxSync hxTrigger hxVals attributes diff --git a/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj b/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj index d1d39b0..6157aad 100644 --- a/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj +++ b/src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj @@ -4,6 +4,7 @@ true Extensions to Giraffe View Engine to support htmx attributes and their values README.md + bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index ba4d623..99cb93f 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -1,110 +1,151 @@ +/// Types and functions supporting htmx attributes in Giraffe View Engine module Giraffe.ViewEngine.Htmx -/// 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 }" - - -/// Valid values for the `hx-encoding` attribute +/// Valid values for the hx-encoding attribute [] module HxEncoding = - /// A standard HTTP form - let Form = "application/x-www-form-urlencoded" + /// A standard HTTP form + [] + let Form = "application/x-www-form-urlencoded" - /// A multipart form (used for file uploads) + /// A multipart form (used for file uploads) + [] let MultipartForm = "multipart/form-data" -/// The events recognized by htmx +/// The events recognized by htmx [] type HxEvent = - /// Send this event to an element to abort a request + + /// Send this event to an element to abort a request | Abort - /// Triggered after an AJAX request has completed processing a successful response + + /// Triggered after an AJAX request has completed processing a successful response | AfterOnLoad - /// Triggered after htmx has initialized a node + + /// Triggered after htmx has initialized a node | AfterProcessNode - /// Triggered after an AJAX request has completed + + /// Triggered after an AJAX request has completed | AfterRequest - /// Triggered after the DOM has settled + + /// Triggered after the DOM has settled | AfterSettle - /// Triggered after new content has been swapped in + + /// Triggered after new content has been swapped in | AfterSwap - /// Triggered before htmx disables an element or removes it from the DOM + + /// Triggered before htmx disables an element or removes it from the DOM | BeforeCleanupElement - /// Triggered before any response processing occurs + + /// Triggered before any response processing occurs | BeforeOnLoad - /// Triggered before htmx initializes a node + + /// Triggered before htmx initializes a node | BeforeProcessNode - /// Triggered before an AJAX request is made + + /// Triggered before an AJAX request is made | BeforeRequest - /// Triggered before a swap is done, allows you to configure the swap + + /// Triggered before a swap is done, allows you to configure the swap | BeforeSwap - /// Triggered just before an ajax request is sent + + /// Triggered just before an ajax request is sent | BeforeSend - /// Triggered before the request, allows you to customize parameters, headers + + /// Triggered before the request, allows you to customize parameters, headers | ConfigRequest + + /// /// Triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request + /// | Confirm - /// Triggered on an error during cache writing + + /// Triggered on an error during cache writing | HistoryCacheError - /// Triggered on a cache miss in the history subsystem + + /// Triggered on a cache miss in the history subsystem | HistoryCacheMiss - /// Triggered on a unsuccessful remote retrieval + + /// Triggered on a unsuccessful remote retrieval | HistoryCacheMissError - /// Triggered on a successful remote retrieval + + /// Triggered on a successful remote retrieval | HistoryCacheMissLoad - /// Triggered when htmx handles a history restoration action + + /// Triggered when htmx handles a history restoration action | HistoryRestore - /// Triggered before content is saved to the history cache + + /// Triggered before content is saved to the history cache | BeforeHistorySave - /// Triggered when new content is added to the DOM + + /// Triggered when new content is added to the DOM | Load + + /// /// Triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined + /// | NoSseSourceError - /// Triggered when an exception occurs during the onLoad handling in htmx + + /// Triggered when an exception occurs during the onLoad handling in htmx | OnLoadError - /// Triggered after an out of band element as been swapped in + + /// Triggered after an out of band element as been swapped in | OobAfterSwap - /// Triggered before an out of band element swap is done, allows you to configure the swap + + /// Triggered before an out of band element swap is done, allows you to configure the swap | OobBeforeSwap - /// Triggered when an out of band element does not have a matching ID in the current DOM + + /// Triggered when an out of band element does not have a matching ID in the current DOM | OobErrorNoTarget - /// Triggered after a prompt is shown + + /// Triggered after a prompt is shown | Prompt - /// Triggered after an url is pushed into history + + /// Triggered after an url is pushed into history | PushedIntoHistory - /// Triggered when an HTTP response error (non-200 or 300 response code) occurs + + /// Triggered when an HTTP response error (non-200 or 300 response code) occurs | ResponseError - /// Triggered when a network error prevents an HTTP request from happening + + /// Triggered when a network error prevents an HTTP request from happening | SendError - /// Triggered when an error occurs with a SSE source + + /// Triggered when an error occurs with a SSE source | SseError - /// Triggered when a SSE source is opened + + /// Triggered when a SSE source is opened | SseOpen - /// Triggered when an error occurs during the swap phase + + /// Triggered when an error occurs during the swap phase | SwapError - /// Triggered when an invalid target is specified + + /// Triggered when an invalid target is specified | TargetError - /// Triggered when a request timeout occurs + + /// Triggered when a request timeout occurs | Timeout - /// Triggered before an element is validated + + /// Triggered before an element is validated | ValidationValidate - /// Triggered when an element fails validation + + /// Triggered when an element fails validation | ValidationFailed - /// Triggered when a request is halted due to validation errors + + /// Triggered when a request is halted due to validation errors | ValidationHalted - /// Triggered when an ajax request aborts + + /// Triggered when an ajax request aborts | XhrAbort - /// Triggered when an ajax request ends + + /// Triggered when an ajax request ends | XhrLoadEnd - /// Triggered when an ajax request starts + + /// Triggered when an ajax request starts | XhrLoadStart - /// Triggered periodically during an ajax request that supports progress events + + /// Triggered periodically during an ajax request that supports progress events | XhrProgress /// The htmx event name (fst) and kebab-case name (snd, for use with hx-on) @@ -153,63 +194,123 @@ type HxEvent = XhrProgress, ("xhr:progress", "xhr:progress") ] - /// The htmx event name + /// 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 = - /// Create headers from a list of key/value pairs - let From = toJson + /// Create headers from a list of key/value pairs + let From = Giraffe.Htmx.Common.toJson -/// Values / helpers for the `hx-params` attribute +/// Values / helpers for the hx-params attribute +/// Documentation [] module HxParams = - /// Include all parameters - let All = "*" + /// Include all parameters + [] + let All = "*" - /// Include no parameters + /// Include no parameters + [] let None = "none" - /// Include the specified parameters - let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") + /// Include the specified parameters + /// One or more fields to include in the request + /// 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 - let Except fields = With fields |> sprintf "not %s" + /// 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" + 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 = - /// Convert a boolean to its lowercase string equivalent - let private toLowerBool (it : bool) = - (string it).ToLowerInvariant () + open Giraffe.Htmx.Common - /// Configure the request with various options - let Configure (opts : string list) = + /// Configure the request with various options + /// The options to configure + /// A string with the configured options + let Configure (opts: string list) = opts |> String.concat ", " |> sprintf "{ %s }" - /// Set a timeout (in milliseconds) - let Timeout (ms : int) = $"\"timeout\": {ms}" + /// Set a timeout (in milliseconds) + /// The milliseconds for the request timeout + /// A string with the configured request timeout + let Timeout (ms: int) = + $"\"timeout\": {ms}" - /// Include or exclude credentials from the request - let Credentials = toLowerBool >> sprintf "\"credentials\": %s" + /// Include or exclude credentials from the request + /// 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 - let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s" + /// Exclude or include headers from the request + /// + /// 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-sync attribute +/// Documentation +[] +module HxSync = + + /// Drop (ignore) this request if a request is already in flight + /// This is the default for hx-sync + [] + let Drop = "drop" + + /// + /// Drop (ignore) this request if a request is already in flight, and if another request occurs while this one is in + /// flight, abort this request + /// + [] + let Abort = "abort" + + /// Abort any current in-flight request and replace it with this one + [] + let Replace = "replace" + + /// Place this request in an element-associated queue + [] + let Queue = "queue" + + /// Queue only the first request received while another request is in flight + [] + let QueueFirst = "queue first" + + /// Queue only the last request received while another request is in flight + [] + let QueueLast = "queue last" + + /// Queue all requests received while another request is in flight + [] + let QueueAll = "queue all" + + +/// Helpers for the hx-trigger attribute +/// Documentation [] module HxTrigger = @@ -221,247 +322,491 @@ module HxTrigger = $"{parts[0]}[{parts[1]}&&{filter}]" | false -> $"{trigger}[{filter}]" - /// Trigger the event on a click + /// Trigger the event on a click + [] let Click = "click" - /// Trigger the event on page load + /// Trigger the event on page load + [] let Load = "load" - /// Trigger the event when the item is visible + /// Trigger the event when the item is visible + [] let Revealed = "revealed" - /// Trigger this event every [timing declaration] - let Every (duration : string) = $"every {duration}" + /// Trigger this event every [timing declaration] + /// The duration on which this trigger should fire (e.g., "1s", "500ms") + /// A trigger timing specification + let Every duration = + $"every %s{duration}" - /// Helpers for defining filters + /// Helpers for defining filters module Filter = - /// Only trigger the event if the `ALT` key is pressed + /// Only trigger the event if the ALT key is pressed let Alt = appendFilter "altKey" - /// Only trigger the event if the `CTRL` key is pressed + /// Only trigger the event if the CTRL key is pressed let Ctrl = appendFilter "ctrlKey" - /// Only trigger the event if the `SHIFT` key is pressed + /// Only trigger the event if the SHIFT key is pressed let Shift = appendFilter "shiftKey" - /// Only trigger the event if `CTRL+ALT` are pressed + /// Only trigger the event if CTRL and ALT are pressed let CtrlAlt = Ctrl >> Alt - /// Only trigger the event if `CTRL+SHIFT` are pressed + /// Only trigger the event if CTRL and SHIFT are pressed let CtrlShift = Ctrl >> Shift - /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed + /// Only trigger the event if CTRL, ALT, and SHIFT are pressed let CtrlAltShift = CtrlAlt >> Shift - /// Only trigger the event if `ALT+SHIFT` are pressed + /// Only trigger the event if ALT and 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" + /// Only trigger once + /// The action which should only be fired once + /// A trigger spec to fire the given action once + let Once action = + appendModifier "once" action - /// Trigger when changed - let Changed = appendModifier "changed" + /// Trigger when changed + /// The element from which the onchange event will be emitted + /// A trigger spec to fire when the given element changes + let Changed elt = + appendModifier "changed" elt - /// Delay execution; resets every time the event is seen - let Delay = sprintf "delay:%s" >> appendModifier + /// Delay execution; resets every time the event is seen + /// The duration for the delay (e.g., "1s", "500ms") + /// The action which should be fired after the given delay + /// A trigger spec to fire the given action after the specified delay + let Delay duration action = + appendModifier $"delay:%s{duration}" action - /// Throttle execution; ignore other events, fire when duration passes - let Throttle = sprintf "throttle:%s" >> appendModifier + /// Throttle execution; ignore other events, fire when duration passes + /// The duration for the throttling (e.g., "1s", "500ms") + /// The action which should be fired after the given duration + /// A trigger spec to fire the given action after the specified duration + let Throttle duration action = + appendModifier $"throttle:%s{duration}" action - /// Trigger this event from a CSS selector - let From = sprintf "from:%s" >> appendModifier + /// Trigger this event from a CSS selector + /// A CSS selector to identify elements which may fire this trigger + /// The action to be fired + /// A trigger spec to fire from the given element(s) + let From selector action = + appendModifier $"from:%s{selector}" action - /// Trigger this event from the `document` object - let FromDocument = From "document" + /// Trigger this event from the document object + /// The action to be fired + /// A trigger spec to fire from the document element + let FromDocument action = + From "document" action - /// Trigger this event from the `window` object - let FromWindow = From "window" + /// Trigger this event from the window object + /// The action to be fired + /// A trigger spec to fire from the window object + let FromWindow action = + From "window" action - /// Trigger this event from the closest parent CSS selector - let FromClosest = sprintf "closest %s" >> From + /// Trigger this event from the closest parent CSS selector + /// The CSS selector from which the action should be fired + /// The action to be fired + /// A trigger spec to fire from the closest element + let FromClosest selector action = + From $"closest %s{selector}" action - /// Trigger this event from the closest child CSS selector - let FromFind = sprintf "find %s" >> From + /// Trigger this event from the closest child CSS selector + /// The child CSS selector from which the action should be fired + /// The action to be fired + /// A trigger spec to fire from the closest child element + let FromFind selector action = + From $"find %s{selector}" action - /// Target the given CSS selector with the results of this event - let Target = sprintf "target:%s" >> appendModifier + /// Target the given CSS selector with the results of this event + /// The CSS selector to which the result of the action will be applied + /// The action to be fired + /// A trigger spec to target the given selector + let Target selector action = + appendModifier $"target:%s{selector}" action - /// Prevent any further events from occurring after this one fires - let Consume = appendModifier "consume" + /// Prevent any further events from occurring after this one fires + /// The action to be fired + /// A trigger spec to fire the given action and prevent further events + let Consume action = + appendModifier "consume" action - /// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" - let Queue = sprintf "queue:%s" >> appendModifier + /// + /// Configure queueing when events fire when others are in flight; if unspecified, the default is last + /// + /// + /// How the request should be queued (consider , , + /// , and ) + /// + /// The action to be fired + /// A trigger spec to queue the given action + let Queue how action = + let qSpec = if how = "" then "" else $":{how}" + appendModifier $"queue{qSpec}" action - /// Queue the first event, discard all others (i.e., a FIFO queue of 1) - let QueueFirst = Queue "first" + /// Queue the first event, discard all others (i.e., a FIFO queue of 1) + /// The action to be fired + /// A trigger spec to queue the given action + let QueueFirst action = + Queue "first" action - /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) - let QueueLast = Queue "last" + /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) + /// The action to be fired + /// A trigger spec to queue the given action + let QueueLast action = + Queue "last" action - /// Queue all events; discard none - let QueueAll = Queue "all" + /// Queue all events; discard none + /// The action to be fired + /// A trigger spec to queue the given action + let QueueAll action = + Queue "all" action - /// Queue no events; discard all - let QueueNone = Queue "none" + /// Queue no events; discard all + /// The action to be fired + /// A trigger spec to queue the given action + let QueueNone action = + Queue "none" action -/// Helper to create the `hx-vals` attribute +/// Helper to create the hx-vals attribute +/// Documentation [] 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 = Giraffe.Htmx.Common.toJson -/// Attributes and flags for htmx +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) - let _hxBoost = attr "hx-boost" "true" + /// Progressively enhances anchors and forms to use AJAX requests + /// Use _hxNoBoost to set to false + /// Documentation + let _hxBoost = attr "hx-boost" "true" - /// Shows a confirm() dialog before issuing a request - let _hxConfirm = attr "hx-confirm" + /// Shows a confirm() dialog before issuing a request + /// The prompt to present to the user when seeking their confirmation + /// A configured hx-confirm attribute + /// Documentation + let _hxConfirm prompt = + attr "hx-confirm" prompt - /// Issues a DELETE to the specified URL - let _hxDelete = attr "hx-delete" + /// 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 - let _hxDisable = flag "hx-disable" + /// 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 - let _hxDisabledElt = attr "hx-disabled-elt" + /// 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 + /// Documentation + let _hxDisabledElt elt = + attr "hx-disabled-elt" elt - /// Disinherit all ("*") or specific htmx attributes - let _hxDisinherit = attr "hx-disinherit" + /// Disinherit all ("*") or specific htmx attributes + /// The htmx attributes to disinherit (should start with "hx-") + /// A configured hx-disinherit attribute + /// Documentation + let _hxDisinherit hxAttrs = + attr "hx-disinherit" hxAttrs - /// Changes the request encoding type - let _hxEncoding = attr "hx-encoding" + /// Changes the request encoding type + /// 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 - let _hxExt = attr "hx-ext" + /// Extensions to use for this element + /// A list of extensions to apply to this element + /// A configured hx-ext attribute + /// Documentation + let _hxExt exts = + attr "hx-ext" exts - /// Issues a GET to the specified URL - let _hxGet = attr "hx-get" + /// 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" + /// Adds to the headers that will be submitted with the request + /// 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" + /// Synchronize events based on another element + /// A CSS selector for the element with which this one should sync + /// The request synchronization action to perform (use HxSync values) + /// A configured hx-sync attribute + /// + /// Documentation + let _hxSync selector action = + attr "hx-sync" $"%s{selector}:%s{action}" - /// 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" + /// Specifies the event that triggers the request + /// The trigger specification (use HxTrigger to create) + /// A configured hx-trigger attribute + /// + /// Documentation + let _hxTrigger spec = + attr "hx-trigger" spec - /// 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 +/// Script tags to pull htmx into a web page module Script = - /// Script tag to load the minified version from unpkg.com + /// Script tag to load the minified version from unpkg.com let minified = - script [ _src "https://unpkg.com/htmx.org@2.0.4" - _integrity "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" + script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.min.js" + _integrity "sha384-Akqfrbj/HpNVo8k11SXBb6TlBWmXXlYQrCSqEWmyKJe+hDm3Z/B2WVG4smwBkRVm" _crossorigin "anonymous" ] [] - /// Script tag to load the unminified version from unpkg.com + /// Script tag to load the unminified version from unpkg.com let unminified = - script [ _src "https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" - _integrity "sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" + script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.6/dist/htmx.js" + _integrity "sha384-ksKjJrwjL5VxqAkAZAVOPXvMkwAykMaNYegdixAESVr+KqLkKE8XBDoZuwyWVUDv" _crossorigin "anonymous" ] [] -/// Functions to extract and render an HTML fragment from a document +/// Functions to extract and render an HTML fragment from a document [] module RenderFragment = /// Does this element have an ID matching the requested ID name? - let private isIdElement nodeId (elt : XmlElement) = + let private isIdElement nodeId (elt: XmlElement) = snd elt |> Array.exists (fun attr -> match attr with @@ -469,62 +814,83 @@ module RenderFragment = | Boolean _ -> false) /// Generate a message if the requested ID node is not found - let private nodeNotFound (nodeId : string) = + let private nodeNotFound (nodeId: string) = $"– ID {nodeId} not found –" - /// Find the node with the named ID - let rec findIdNode nodeId (node : XmlNode) : XmlNode option = + /// Find the node with the named ID + /// The id attribute to find + /// The node tree to search + /// 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 | VoidElement elt -> if isIdElement nodeId elt then Some node else None | ParentNode (elt, children) -> - if isIdElement nodeId elt then Some node else children |> List.tryPick (fun c -> findIdNode nodeId c) + if isIdElement nodeId elt then Some node else children |> List.tryPick (findIdNode nodeId) - /// Functions to render a fragment as a string + /// Functions to render a fragment as a string [] module AsString = - /// Render to HTML for the given ID - let htmlFromNodes nodeId (nodes : XmlNode list) = - match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with + /// Render to HTML for the given ID + /// 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 + 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 + /// Render to HTML for the given ID + /// 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 let htmlFromNode nodeId node = match findIdNode nodeId node with | Some idNode -> RenderView.AsString.htmlNode idNode | None -> nodeNotFound nodeId - /// Functions to render a fragment as bytes + /// Functions to render a fragment as bytes [] module AsBytes = let private utf8 = System.Text.Encoding.UTF8 - /// Render to HTML for the given ID - let htmlFromNodes nodeId (nodes : XmlNode list) = - match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with + /// Render to bytes for the given ID + /// 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 + 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 HTML for the given ID + /// Render to bytes for the given ID + /// 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 let htmlFromNode nodeId node = match findIdNode nodeId node with | Some idNode -> RenderView.AsBytes.htmlNode idNode | None -> nodeNotFound nodeId |> utf8.GetBytes - /// Functions to render a fragment into a StringBuilder + /// Functions to render a fragment into a StringBuilder [] module IntoStringBuilder = - /// Render to HTML for the given ID - let htmlFromNodes sb nodeId (nodes : XmlNode list) = - match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with + /// 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 to HTML for the given ID + /// 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 | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode diff --git a/src/ViewEngine.Htmx/README.md b/src/ViewEngine.Htmx/README.md index 7f17761..10b2c77 100644 --- a/src/ViewEngine.Htmx/README.md +++ b/src/ViewEngine.Htmx/README.md @@ -2,7 +2,7 @@ This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine. -**htmx version: 2.0.4** +**htmx version: 2.0.6** _Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_ @@ -29,7 +29,7 @@ Support modules include: - `HxTrigger` - `HxVals` -There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging). +There are two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging). This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).