2 Commits

Author SHA1 Message Date
5e28714215 WIP on docs
- bump version
2024-12-30 22:06:27 -05:00
28cec1cb70 Drop .NET 6 support (#14) 2024-12-30 22:05:52 -05:00
13 changed files with 326 additions and 745 deletions

View File

@@ -14,8 +14,6 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
|---|---| |---|---|
|[![Nuget](https://img.shields.io/nuget/v/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/)|[![Nuget](https://img.shields.io/nuget/v/Giraffe.ViewEngine.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)| |[![Nuget](https://img.shields.io/nuget/v/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/)|[![Nuget](https://img.shields.io/nuget/v/Giraffe.ViewEngine.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)|
Both of these packages will also install `Giraffe.Htmx.Common`, which has some common definitions and provides a local-to-your-project version of the htmx JavaScript _(as of v2.0.8)_.
## Server Side (`Giraffe.Htmx`) ## Server Side (`Giraffe.Htmx`)
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request. In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request.
@@ -44,8 +42,6 @@ let theHandler : HttpHandler =
Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well. Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well.
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library. This can be used in code, Razor templates, etc. (If you're using Giraffe.ViewEngine, see below.)
## View Engine (`Giraffe.ViewEngine.Htmx`) ## View Engine (`Giraffe.ViewEngine.Htmx`)
As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible. As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible.
@@ -70,7 +66,7 @@ let shiftClick =
] ]
``` ```
If you want to use the package-provided htmx library, `Htmx.Script.local` will create the `script` tag for you. To load htmx from jsDelivr, `Htmx.Script.cdnMinified` or `Htmx.Script.cdnUnminified` can be used to load the script in your HTML trees. In this case, if you are using a Content Security Policy (CSP) header, `cdn.jsdelivr.net` will need to be added to the `script-src` list. If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees.
## Feedback / Help ## Feedback / Help

View File

@@ -2,12 +2,6 @@
[<AutoOpen>] [<AutoOpen>]
module Giraffe.Htmx.Common module Giraffe.Htmx.Common
/// <summary>The version of htmx embedded in the package</summary>
let HtmxVersion = "2.0.8"
/// <summary>The path for the provided htmx script</summary>
let internal htmxLocalScript = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
/// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary> /// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary>
/// <param name="pairs">The key/value pairs to be serialized to JSON</param> /// <param name="pairs">The key/value pairs to be serialized to JSON</param>
/// <returns>A string with the key/value pairs serialized to JSON</returns> /// <returns>A string with the key/value pairs serialized to JSON</returns>
@@ -17,43 +11,35 @@ let internal toJson (pairs: (string * string) list) =
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
/// <summary>Convert a boolean to lowercase "true" or "false"</summary> /// <summary>Convert a boolean to lowercase <tt>true</tt> or <tt>false</tt></summary>
/// <param name="boolValue">The boolean value to convert</param> /// <param name="boolValue">The boolean value to convert</param>
/// <returns>"true" for <c>true</c>, "false" for <c>false</c></returns> /// <returns>"true" for <tt>true</tt>, "false" for <tt>false</tt></returns>
let internal toLowerBool (boolValue: bool) = let internal toLowerBool (boolValue: bool) =
(string boolValue).ToLowerInvariant() (string boolValue).ToLowerInvariant()
/// <summary>Valid values for the <c>hx-swap</c> attribute / <c>HX-Reswap</c> header</summary> /// <summary>Valid values for the <tt>hx-swap</tt> attribute / <tt>HX-Reswap</tt> header</summary>
/// <remarks>May be combined with <c>swap</c> / <c>settle</c> / <c>scroll</c> / <c>show</c> config)</remarks> /// <remarks>May be combined with <tt>swap</tt> / <tt>settle</tt> / <tt>scroll</tt> / <tt>show</tt> config)</remarks>
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxSwap = module HxSwap =
/// <summary>The default, replace the inner HTML of the target element</summary> /// <summary>The default, replace the inner HTML of the target element</summary>
[<Literal>]
let InnerHtml = "innerHTML" let InnerHtml = "innerHTML"
/// <summary>Replace the entire target element with the response</summary> /// <summary>Replace the entire target element with the response</summary>
[<Literal>]
let OuterHtml = "outerHTML" let OuterHtml = "outerHTML"
/// <summary>Insert the response before the target element</summary> /// <summary>Insert the response before the target element</summary>
[<Literal>]
let BeforeBegin = "beforebegin" let BeforeBegin = "beforebegin"
/// <summary>Insert the response before the first child of the target element</summary> /// <summary>Insert the response before the first child of the target element</summary>
[<Literal>]
let AfterBegin = "afterbegin" let AfterBegin = "afterbegin"
/// <summary>Insert the response after the last child of the target element</summary> /// <summary>Insert the response after the last child of the target element</summary>
[<Literal>]
let BeforeEnd = "beforeend" let BeforeEnd = "beforeend"
/// <summary>Insert the response after the target element</summary> /// <summary>Insert the response after the target element</summary>
[<Literal>]
let AfterEnd = "afterend" let AfterEnd = "afterend"
/// <summary>Does not append content from response (out of band items will still be processed).</summary> /// <summary>Does not append content from response (out of band items will still be processed).</summary>
[<Literal>]
let None = "none" let None = "none"

View File

@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Razor"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>

View File

@@ -1,5 +1,5 @@
## Giraffe.Htmx.Common ## Giraffe.Htmx.Common
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx as a static web asset, allowing it to be loaded from your local (or published) project. 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.8** **htmx version: 2.0.4**

File diff suppressed because one or more lines are too long

View File

@@ -1,18 +1,10 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0</TargetFrameworks>
<VersionPrefix>2.0.8</VersionPrefix> <VersionPrefix>2.0.5</VersionPrefix>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>- Adds package-provided script (available via Giraffe.Htmx.Common); use app.MapStaticAssets() for publish support, use builder.UseStaticWebAssets() for non-development/non-published execution <PackageReleaseNotes>Add full packaged XML documentation; update script tags to pull htmx 2.0.5 (no header or attribute changes)</PackageReleaseNotes>
- [View Engine] Deprecates Script.minified and Script.unminified (use Script.cdnMinified and Script.cdnUnminified instead)
- Updates script tags to pull htmx 2.0.8 (no header or attribute changes)
- Adds .NET 10 support
See full release notes, including more info about the package-provided script, at https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/releases/tag/v2.0.8
NOTE: As of 2.0.6, the CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed script-src domains accordingly
</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</PackageProjectUrl> <PackageProjectUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</PackageProjectUrl>

View File

@@ -11,7 +11,7 @@ let private hdr (headers : IHeaderDictionary) hdr =
/// Extensions to the header dictionary /// Extensions to the header dictionary
type IHeaderDictionary with type IHeaderDictionary with
/// <summary>Indicates that the request is via an element using <c>hx-boost</c></summary> /// <summary>Indicates that the request is via an element using <tt>hx-boost</tt></summary>
member this.HxBoosted member this.HxBoosted
with get () = hdr this "HX-Boosted" |> Option.map bool.Parse with get () = hdr this "HX-Boosted" |> Option.map bool.Parse
@@ -19,27 +19,29 @@ type IHeaderDictionary with
member this.HxCurrentUrl member this.HxCurrentUrl
with get () = hdr this "HX-Current-URL" |> Option.map Uri with get () = hdr this "HX-Current-URL" |> Option.map Uri
/// <summary><c>true</c> if the request is for history restoration after a miss in the local history cache</summary> /// <summary>
/// <tt>true</tt> if the request is for history restoration after a miss in the local history cache
/// </summary>
member this.HxHistoryRestoreRequest member this.HxHistoryRestoreRequest
with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
/// <summary>The user response to an <c>hx-prompt</c></summary> /// <summary>The user response to an <tt>hx-prompt</tt></summary>
member this.HxPrompt member this.HxPrompt
with get () = hdr this "HX-Prompt" with get () = hdr this "HX-Prompt"
/// <summary><c>true</c> if the request came from htmx</summary> /// <summary><tt>true</tt> if the request came from htmx</summary>
member this.HxRequest member this.HxRequest
with get () = hdr this "HX-Request" |> Option.map bool.Parse with get () = hdr this "HX-Request" |> Option.map bool.Parse
/// <summary>The <c>id</c> attribute of the target element if it exists</summary> /// <summary>The <tt>id</tt> attribute of the target element if it exists</summary>
member this.HxTarget member this.HxTarget
with get () = hdr this "HX-Target" with get () = hdr this "HX-Target"
/// <summary>The <c>id</c> attribute of the triggered element if it exists</summary> /// <summary>The <tt>id</tt> attribute of the triggered element if it exists</summary>
member this.HxTrigger member this.HxTrigger
with get () = hdr this "HX-Trigger" with get () = hdr this "HX-Trigger"
/// <summary>The <c>name</c> attribute of the triggered element if it exists</summary> /// <summary>The <tt>name</tt> attribute of the triggered element if it exists</summary>
member this.HxTriggerName member this.HxTriggerName
with get () = hdr this "HX-Trigger-Name" with get () = hdr this "HX-Trigger-Name"
@@ -62,123 +64,99 @@ module Handlers =
open Giraffe.Htmx.Common open Giraffe.Htmx.Common
/// <summary>Instruct htmx to perform a client-side redirect for content</summary>
/// <param name="path">The path where the content should be found</param>
/// <returns>An HTTP handler with the <c>HX-Location</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-location/">Documentation</seealso>
let withHxLocation (path: string) : HttpHandler =
setHttpHeader "HX-Location" path
/// <summary>Pushes a new url into the history stack</summary> /// <summary>Pushes a new url into the history stack</summary>
/// <param name="url">The URL to be pushed</param> /// <param name="url">The URL to be pushed</param>
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Push-Url</tt> header set</returns>
/// <remarks>Use <see cref="withHxNoPushUrl" /> to explicitly not push a new URL</remarks>
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
let withHxPushUrl (url: string) : HttpHandler = let withHxPushUrl (url: string) : HttpHandler =
setHttpHeader "HX-Push-Url" url setHttpHeader "HX-Push-Url" url
/// <summary>Explicitly do not push a new URL into the history stack</summary> /// <summary>Explicitly do not push a new URL into the history stack</summary>
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set to <c>false</c></returns> /// <returns>An HTTP handler with the <tt>HX-Push-Url</tt> header set to <tt>false</tt></returns>
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
let withHxNoPushUrl : HttpHandler = let withHxNoPushUrl : HttpHandler =
toLowerBool false |> withHxPushUrl toLowerBool false |> withHxPushUrl
/// Pushes a new url into the history stack
[<Obsolete "Use withHxPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
let withHxPush = withHxPushUrl
/// Explicitly do not push a new URL into the history stack
[<Obsolete "Use withHxNoPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
let withHxNoPush = withHxNoPushUrl
/// <summary>Can be used to do a client-side redirect to a new location</summary> /// <summary>Can be used to do a client-side redirect to a new location</summary>
/// <param name="url">The URL to which the client should be redirected</param> /// <param name="url">The URL to which the client should be redirected</param>
/// <returns>An HTTP handler with the <c>HX-Redirect</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Redirect</tt> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-redirect/">Documentation</seealso>
let withHxRedirect (url: string) : HttpHandler = let withHxRedirect (url: string) : HttpHandler =
setHttpHeader "HX-Redirect" url setHttpHeader "HX-Redirect" url
/// <summary>If set to <c>true</c> the client side will do a full refresh of the page</summary> /// <summary>If set to <tt>true</tt> the client side will do a full refresh of the page</summary>
/// <param name="shouldRefresh">Whether the client should refresh their page</param> /// <param name="shouldRefresh">Whether the client should refresh their page</param>
/// <returns>An HTTP handler with the <c>HX-Refresh</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Refresh</tt> header set</returns>
let withHxRefresh shouldRefresh : HttpHandler = let withHxRefresh shouldRefresh : HttpHandler =
(toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh (toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh
/// <summary>Replaces the current URL in the history stack</summary> /// <summary>Replaces the current URL in the history stack</summary>
/// <param name="url">The URL to place in the history stack in place of the current one</param> /// <param name="url">The URL to place in the history stack in place of the current one</param>
/// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Replace-URL</tt> header set</returns>
/// <remarks>Use <see cref="withHxNoRelaceUrl" /> to explicitly not replace the current URL</remarks>
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
let withHxReplaceUrl url : HttpHandler = let withHxReplaceUrl url : HttpHandler =
setHttpHeader "HX-Replace-Url" url setHttpHeader "HX-Replace-Url" url
/// <summary>Explicitly do not replace the current URL in the history stack</summary> /// <summary>Explicitly do not replace the current URL in the history stack</summary>
/// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set to <c>false</c></returns> /// <returns>An HTTP handler with the <tt>HX-Replace-URL</tt> header set to <tt>false</tt></returns>
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
let withHxNoReplaceUrl : HttpHandler = let withHxNoReplaceUrl : HttpHandler =
toLowerBool false |> withHxReplaceUrl toLowerBool false |> withHxReplaceUrl
/// <summary>Override which portion of the response will be swapped into the target document</summary> /// <summary>Override which portion of the response will be swapped into the target document</summary>
/// <param name="target">The selector for the new response target</param> /// <param name="target">The selector for the new response target</param>
/// <returns>An HTTP handler with the <c>HX-Reselect</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Reselect</tt> header set</returns>
let withHxReselect (target: string) : HttpHandler = let withHxReselect (target: string) : HttpHandler =
setHttpHeader "HX-Reselect" target setHttpHeader "HX-Reselect" target
/// <summary>Override the <c>hx-swap</c> attribute from the initiating element</summary> /// <summary>Override the <tt>hx-swap</tt> attribute from the initiating element</summary>
/// <param name="swap">The swap value to override</param> /// <param name="swap">The swap value to override</param>
/// <returns>An HTTP handler with the <c>HX-Reswap</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Reswap</tt> header set</returns>
/// <remarks>Use <see cref="T:Giraffe.Htmx.Common.HxSwap">HxSwap</see> constants for best results</remarks> /// <remarks>Use <see cref="T:Giraffe.Htmx.Common.HxSwap">HxSwap</see> constants for best results</remarks>
let withHxReswap (swap: string) : HttpHandler = let withHxReswap (swap: string) : HttpHandler =
setHttpHeader "HX-Reswap" swap setHttpHeader "HX-Reswap" swap
/// <summary>Allows you to override the <c>hx-target</c> attribute</summary> /// <summary>Allows you to override the <tt>hx-target</tt> attribute</summary>
/// <param name="target">The new target for the response</param> /// <param name="target">The new target for the response</param>
/// <returns>An HTTP handler with the <c>HX-Retarget</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Retarget</tt> header set</returns>
let withHxRetarget (target: string) : HttpHandler = let withHxRetarget (target: string) : HttpHandler =
setHttpHeader "HX-Retarget" target setHttpHeader "HX-Retarget" target
/// <summary>Allows you to trigger a single client side event</summary> /// <summary>Allows you to trigger a single client side event</summary>
/// <param name="evt">The call to the event that should be triggered</param> /// <param name="evt">The call to the event that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger</tt> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTrigger (evt: string) : HttpHandler = let withHxTrigger (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger" evt setHttpHeader "HX-Trigger" evt
/// <summary>Allows you to trigger multiple client side events</summary> /// <summary>Allows you to trigger multiple client side events</summary>
/// <param name="evts">The calls to events that should be triggered</param> /// <param name="evts">The calls to events that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger</c> header set for all given events</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger</tt> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerMany evts : HttpHandler = let withHxTriggerMany evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger" toJson evts |> setHttpHeader "HX-Trigger"
/// <summary>Allows you to trigger a single client side event after changes have settled</summary> /// <summary>Allows you to trigger a single client side event after changes have settled</summary>
/// <param name="evt">The call to the event that should be triggered</param> /// <param name="evt">The call to the event that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger-After-Settle</tt> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerAfterSettle (evt: string) : HttpHandler = let withHxTriggerAfterSettle (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger-After-Settle" evt setHttpHeader "HX-Trigger-After-Settle" evt
/// <summary>Allows you to trigger multiple client side events after changes have settled</summary> /// <summary>Allows you to trigger multiple client side events after changes have settled</summary>
/// <param name="evts">The calls to events that should be triggered</param> /// <param name="evts">The calls to events that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set for all given events</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger-After-Settle</tt> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerManyAfterSettle evts : HttpHandler = let withHxTriggerManyAfterSettle evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle" toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
/// <summary>Allows you to trigger a single client side event after DOM swapping occurs</summary> /// <summary>Allows you to trigger a single client side event after DOM swapping occurs</summary>
/// <param name="evt">The call to the event that should be triggered</param> /// <param name="evt">The call to the event that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger-After-Swap</tt> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerAfterSwap (evt: string) : HttpHandler = let withHxTriggerAfterSwap (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger-After-Swap" evt setHttpHeader "HX-Trigger-After-Swap" evt
/// <summary>Allows you to trigger multiple client side events after DOM swapping occurs</summary> /// <summary>Allows you to trigger multiple client side events after DOM swapping occurs</summary>
/// <param name="evts">The calls to events that should be triggered</param> /// <param name="evts">The calls to events that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set for all given events</returns> /// <returns>An HTTP handler with the <tt>HX-Trigger-After-Swap</tt> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerManyAfterSwap evts : HttpHandler = let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap" toJson evts |> setHttpHeader "HX-Trigger-After-Swap"
/// <summary>Load the package-provided version of the htmx script</summary>
[<RequireQualifiedAccess>]
module HtmxScript =
open Giraffe.Htmx.Common
open Microsoft.AspNetCore.Html
/// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary>
let local = HtmlString $"""<script src="{htmxLocalScript}"></script>"""

View File

@@ -2,7 +2,7 @@
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`. This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 2.0.8** **htmx version: 2.0.4**
_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.*._ _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.*._
@@ -34,8 +34,6 @@ let myHandler : HttpHander =
The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information. The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information.
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`.
### Learn ### Learn
The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events. The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events.

View File

@@ -3,12 +3,6 @@ module Common
open Expecto open Expecto
open Giraffe.Htmx open Giraffe.Htmx
/// Test to ensure the version was updated
let version =
test "HtmxVersion is correct" {
Expect.equal HtmxVersion "2.0.8" "htmx version incorrect"
}
/// Tests for the HxSwap module /// Tests for the HxSwap module
let swap = let swap =
testList "HxSwap" [ testList "HxSwap" [
@@ -36,4 +30,4 @@ let swap =
] ]
/// All tests for this module /// All tests for this module
let allTests = testList "Htmx.Common" [ version; swap ] let allTests = testList "Htmx.Common" [ swap ]

View File

@@ -3,7 +3,6 @@ module Htmx
open System open System
open Expecto open Expecto
open Giraffe.Htmx open Giraffe.Htmx
open Microsoft.AspNetCore.Html
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open NSubstitute open NSubstitute
@@ -12,22 +11,22 @@ let dictExtensions =
testList "IHeaderDictionaryExtensions" [ testList "IHeaderDictionaryExtensions" [
testList "HxBoosted" [ testList "HxBoosted" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Boosted", "true") dic.Add ("HX-Boosted", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" 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" Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Boosted", "false") dic.Add ("HX-Boosted", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" 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" Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false"
@@ -35,14 +34,14 @@ let dictExtensions =
] ]
testList "HxCurrentUrl" [ testList "HxCurrentUrl" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Current-URL", "http://localhost/test.htm") dic.Add ("HX-Current-URL", "http://localhost/test.htm")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present" Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present"
Expect.equal Expect.equal
@@ -52,22 +51,22 @@ let dictExtensions =
] ]
testList "HxHistoryRestoreRequest" [ testList "HxHistoryRestoreRequest" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-History-Restore-Request", "true") dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" 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" Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-History-Restore-Request", "false") dic.Add ("HX-History-Restore-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
Expect.isFalse Expect.isFalse
@@ -76,14 +75,14 @@ let dictExtensions =
] ]
testList "HxPrompt" [ testList "HxPrompt" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Prompt", "of course") dic.Add ("HX-Prompt", "of course")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present" 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" Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect"
@@ -91,22 +90,22 @@ let dictExtensions =
] ]
testList "HxRequest" [ testList "HxRequest" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" 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" Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Request", "false") dic.Add ("HX-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" 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" Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false"
@@ -114,14 +113,14 @@ let dictExtensions =
] ]
testList "HxTarget" [ testList "HxTarget" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Target", "#leItem") dic.Add ("HX-Target", "#leItem")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present" 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" Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect"
@@ -129,14 +128,14 @@ let dictExtensions =
] ]
testList "HxTrigger" [ testList "HxTrigger" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Trigger", "#trig") dic.Add ("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present" 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" Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect"
@@ -144,14 +143,14 @@ let dictExtensions =
] ]
testList "HxTriggerName" [ testList "HxTriggerName" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned"
} }
test "HxTriggerName succeeds when the header is present" { test "HxTriggerName succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Trigger-Name", "click") dic.Add ("HX-Trigger-Name", "click")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present" 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" Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect"
@@ -164,36 +163,36 @@ let reqExtensions =
testList "HttpRequestExtensions" [ testList "HttpRequestExtensions" [
testList "IsHtmx" [ testList "IsHtmx" [
test "succeeds when request is not from htmx" { test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request" Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request"
} }
test "succeeds when request is from htmx" { test "succeeds when request is from htmx" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request" Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request"
} }
] ]
testList "IsHtmxRefresh" [ testList "IsHtmxRefresh" [
test "succeeds when request is not from htmx" { test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" 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" { test "succeeds when request is from htmx, but not a refresh" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Request", "true") dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" 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" { test "IsHtmxRefresh succeeds when request is from htmx and is a refresh" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
dic.Add("HX-Request", "true") dic.Add ("HX-Request", "true")
dic.Add("HX-History-Restore-Request", "true") dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh" Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh"
} }
@@ -203,38 +202,30 @@ let reqExtensions =
open System.Threading.Tasks open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate /// Dummy "next" parameter to get the pipeline to execute/terminate
let next (ctx: HttpContext) = Task.FromResult(Some ctx) let next (ctx : HttpContext) = Task.FromResult (Some ctx)
/// Tests for the HttpHandler functions provided in the Handlers module /// Tests for the HttpHandler functions provided in the Handlers module
let handlers = let handlers =
testList "HandlerTests" [ testList "HandlerTests" [
testTask "withHxLocation succeeds" {
let ctx = Substitute.For<HttpContext>()
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" { testTask "withHxPushUrl succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxPushUrl "/a-new-url" next ctx let! _ = withHxPushUrl "/a-new-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" 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" Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect"
} }
testTask "withHxNoPushUrl succeeds" { testTask "withHxNoPushUrl succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoPushUrl next ctx let! _ = withHxNoPushUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" 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" Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect"
} }
testTask "withHxRedirect succeeds" { testTask "withHxRedirect succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRedirect "/somewhere-else" next ctx let! _ = withHxRedirect "/somewhere-else" next ctx
Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present" Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present"
@@ -242,16 +233,16 @@ let handlers =
} }
testList "withHxRefresh" [ testList "withHxRefresh" [
testTask "succeeds when set to true" { testTask "succeeds when set to true" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh true next ctx let! _ = withHxRefresh true next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" 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" Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect"
} }
testTask "succeeds when set to false" { testTask "succeeds when set to false" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh false next ctx let! _ = withHxRefresh false next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
@@ -259,56 +250,56 @@ let handlers =
} }
] ]
testTask "withHxReplaceUrl succeeds" { testTask "withHxReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReplaceUrl "/a-substitute-url" next ctx let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" 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" Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect"
} }
testTask "withHxNoReplaceUrl succeeds" { testTask "withHxNoReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoReplaceUrl next ctx let! _ = withHxNoReplaceUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" 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" Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect"
} }
testTask "withHxReselect succeeds" { testTask "withHxReselect succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReselect "#test" next ctx let! _ = withHxReselect "#test" next ctx
Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present" 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" Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect"
} }
testTask "withHxReswap succeeds" { testTask "withHxReswap succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReswap HxSwap.BeforeEnd next ctx let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present" 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" Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect"
} }
testTask "withHxRetarget succeeds" { testTask "withHxRetarget succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRetarget "#somewhereElse" next ctx let! _ = withHxRetarget "#somewhereElse" next ctx
Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present" 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" Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect"
} }
testTask "withHxTrigger succeeds" { testTask "withHxTrigger succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTrigger "doSomething" next ctx let! _ = withHxTrigger "doSomething" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" 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" Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect"
} }
testTask "withHxTriggerMany succeeds" { testTask "withHxTriggerMany succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
@@ -316,8 +307,8 @@ let handlers =
dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect" dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect"
} }
testTask "withHxTriggerAfterSettle succeeds" { testTask "withHxTriggerAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Expect.isTrue Expect.isTrue
@@ -325,8 +316,8 @@ let handlers =
Expect.equal dic["HX-Trigger-After-Settle"].[0] "byTheWay" "The HX-Trigger-After-Settle value was incorrect" Expect.equal dic["HX-Trigger-After-Settle"].[0] "byTheWay" "The HX-Trigger-After-Settle value was incorrect"
} }
testTask "withHxTriggerManyAfterSettle succeeds" { testTask "withHxTriggerManyAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Expect.isTrue Expect.isTrue
@@ -336,16 +327,16 @@ let handlers =
"The HX-Trigger-After-Settle value was incorrect" "The HX-Trigger-After-Settle value was incorrect"
} }
testTask "withHxTriggerAfterSwap succeeds" { testTask "withHxTriggerAfterSwap succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSwap "justASec" next ctx let! _ = withHxTriggerAfterSwap "justASec" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present" 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" Expect.equal dic["HX-Trigger-After-Swap"].[0] "justASec" "The HX-Trigger-After-Swap value was incorrect"
} }
testTask "withHxTriggerManyAfterSwap succeeds" { testTask "withHxTriggerManyAfterSwap succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary() let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx 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" Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present"
@@ -355,16 +346,5 @@ let handlers =
} }
] ]
/// Tests for the HtmxScript module
let script =
testList "HtmxScript" [
test "local generates correct link" {
Expect.equal
(string HtmxScript.local)
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
"htmx script link is incorrect"
}
]
/// All tests for this module /// All tests for this module
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script ] let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers ]

View File

@@ -406,8 +406,8 @@ let hxEvent =
Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct" Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct"
} }
] ]
] ]
/// Tests for the HxHeaders module /// Tests for the HxHeaders module
let hxHeaders = let hxHeaders =
testList "HxHeaders" [ testList "HxHeaders" [
@@ -497,32 +497,6 @@ 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 /// Tests for the HxTrigger module
let hxTrigger = let hxTrigger =
testList "HxTrigger" [ testList "HxTrigger" [
@@ -662,9 +636,6 @@ let hxTrigger =
test "succeeds when it is not the first modifier" { test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" 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" [ testList "QueueFirst" [
test "succeeds when it is the first modifier" { test "succeeds when it is the first modifier" {
@@ -755,7 +726,7 @@ let attributes =
|> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>""" |> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>"""
} }
test "_hxHistory succeeds" { test "_hxHistory succeeds" {
span [ _hxHistory false ] [] |> shouldRender """<span hx-history="false"></span>""" span [ _hxHistory "false" ] [] |> shouldRender """<span hx-history="false"></span>"""
} }
test "_hxHistoryElt succeeds" { test "_hxHistoryElt succeeds" {
table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>""" table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>"""
@@ -786,7 +757,7 @@ let attributes =
hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">""" hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">"""
} }
test "_hxPreserve succeeds" { test "_hxPreserve succeeds" {
img [ _hxPreserve ] |> shouldRender """<img hx-preserve>""" img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">"""
} }
test "_hxPrompt succeeds" { test "_hxPrompt succeeds" {
strong [ _hxPrompt "Who goes there?" ] [] strong [ _hxPrompt "Who goes there?" ] []
@@ -821,8 +792,7 @@ let attributes =
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>""" li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
} }
test "_hxSync succeeds" { test "_hxSync succeeds" {
nav [ _hxSync "closest form" HxSync.Abort ] [] nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
|> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
} }
test "_hxTarget succeeds" { test "_hxTarget succeeds" {
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>""" header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
@@ -842,31 +812,22 @@ let attributes =
} }
] ]
open Giraffe.Htmx.Common
/// Tests for the Script module /// Tests for the Script module
let script = let script =
testList "Script" [ testList "Script" [
test "local succeeds" { test "minified succeeds" {
let html = RenderView.AsString.htmlNode Script.local let html = RenderView.AsString.htmlNode Script.minified
Expect.equal Expect.equal
html html
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>""" """<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>"""
"Local script tag is incorrect" "Minified script tag is incorrect"
} }
test "cdnMinified succeeds" { test "unminified succeeds" {
let html = RenderView.AsString.htmlNode Script.cdnMinified let html = RenderView.AsString.htmlNode Script.unminified
Expect.equal Expect.equal
html html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha256-Iig+9oy3VFkU8KiKG97cclanA9HVgMHSVSF9ClDTExM=" crossorigin="anonymous"></script>""" """<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>"""
"CDN minified script tag is incorrect" "Unminified script tag is incorrect"
}
test "cdnUnminified succeeds" {
let html = RenderView.AsString.htmlNode Script.cdnUnminified
Expect.equal
html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha256-upUwYnay6R+DA68rROTAP+EdfO3NvOqtE513PgDyAYM=" crossorigin="anonymous"></script>"""
"CDN unminified script tag is incorrect"
} }
] ]
@@ -878,7 +839,7 @@ let renderFragment =
/// Validate that the two object references are the same object /// Validate that the two object references are the same object
let isSame obj1 obj2 message = let isSame obj1 obj2 message =
Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message Expect.isTrue (obj.ReferenceEquals (obj1, obj2)) message
testList "findIdNode" [ testList "findIdNode" [
test "fails with a Text node" { test "fails with a Text node" {
@@ -960,7 +921,7 @@ let renderFragment =
} }
test "fails when an ID is not matched" { test "fails when an ID is not matched" {
Expect.equal Expect.equal
(RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes(nodeNotFound "whiff")) (RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes (nodeNotFound "whiff"))
"HTML bytes are incorrect" "HTML bytes are incorrect"
} }
] ]
@@ -977,7 +938,7 @@ let renderFragment =
} }
test "fails when an ID is not matched" { test "fails when an ID is not matched" {
Expect.equal Expect.equal
(RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes(nodeNotFound "foo")) (RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes (nodeNotFound "foo"))
"HTML bytes are incorrect" "HTML bytes are incorrect"
} }
] ]
@@ -985,31 +946,31 @@ let renderFragment =
testList "IntoStringBuilder" [ testList "IntoStringBuilder" [
testList "htmlFromNodes" [ testList "htmlFromNodes" [
test "succeeds when an ID is matched" { test "succeeds when an ID is matched" {
let sb = StringBuilder() let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me" RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me"
[ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]] [ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]]
Expect.equal (string sb) """<span id="find-me">;)</span>""" "HTML is incorrect" Expect.equal (string sb) """<span id="find-me">;)</span>""" "HTML is incorrect"
} }
test "fails when an ID is not matched" { test "fails when an ID is not matched" {
let sb = StringBuilder() let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" [] RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" []
Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect" Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect"
} }
] ]
testList "htmlFromNode" [ testList "htmlFromNode" [
test "succeeds when ID is matched at top level" { 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" ]) RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ])
Expect.equal (string sb) """<p id="top">pinnacle</p>""" "HTML is incorrect" Expect.equal (string sb) """<p id="top">pinnacle</p>""" "HTML is incorrect"
} }
test "succeeds when ID is matched in child element" { 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" ]] div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]]
|> RenderFragment.IntoStringBuilder.htmlFromNode sb "it" |> RenderFragment.IntoStringBuilder.htmlFromNode sb "it"
Expect.equal (string sb) """<p id="it">is here</p>""" "HTML is incorrect" Expect.equal (string sb) """<p id="it">is here</p>""" "HTML is incorrect"
} }
test "fails when an ID is not matched" { test "fails when an ID is not matched" {
let sb = StringBuilder() let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr []) RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr [])
Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect" Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect"
} }
@@ -1025,7 +986,6 @@ let allTests =
hxHeaders hxHeaders
hxParams hxParams
hxRequest hxRequest
hxSync
hxTrigger hxTrigger
hxVals hxVals
attributes attributes

View File

@@ -1,16 +1,14 @@
/// <summary>Types and functions supporting htmx attributes in Giraffe View Engine</summary> /// <summary>Types and functions supporting htmx attributes in Giraffe View Engine</summary>
module Giraffe.ViewEngine.Htmx module Giraffe.ViewEngine.Htmx
/// <summary>Valid values for the <c>hx-encoding</c> attribute</summary> /// <summary>Valid values for the <tt>hx-encoding</tt> attribute</summary>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxEncoding = module HxEncoding =
/// <summary>A standard HTTP form</summary> /// <summary>A standard HTTP form</summary>
[<Literal>]
let Form = "application/x-www-form-urlencoded" let Form = "application/x-www-form-urlencoded"
/// <summary>A multipart form (used for file uploads)</summary> /// <summary>A multipart form (used for file uploads)</summary>
[<Literal>]
let MultipartForm = "multipart/form-data" let MultipartForm = "multipart/form-data"
@@ -197,11 +195,11 @@ type HxEvent =
/// <summary>The htmx event name</summary> /// <summary>The htmx event name</summary>
override this.ToString() = fst HxEvent.Values[this] override this.ToString() = fst HxEvent.Values[this]
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary> /// <summary>The <tt>hx-on</tt> variant of the htmx event name</summary>
member this.ToHxOnString() = snd HxEvent.Values[this] member this.ToHxOnString() = snd HxEvent.Values[this]
/// <summary>Helper to create the <c>hx-headers</c> attribute</summary> /// <summary>Helper to create the <tt>hx-headers</tt> attribute</summary>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxHeaders = module HxHeaders =
@@ -209,34 +207,30 @@ module HxHeaders =
let From = Giraffe.Htmx.Common.toJson let From = Giraffe.Htmx.Common.toJson
/// <summary>Values / helpers for the <c>hx-params</c> attribute</summary> /// <summary>Values / helpers for the <tt>hx-params</tt> attribute</summary>
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxParams = module HxParams =
/// <summary>Include all parameters</summary> /// <summary>Include all parameters</summary>
[<Literal>]
let All = "*" let All = "*"
/// <summary>Include no parameters</summary> /// <summary>Include no parameters</summary>
[<Literal>]
let None = "none" let None = "none"
/// <summary>Include the specified parameters</summary> /// <summary>Include the specified parameters</summary>
/// <param name="fields">One or more fields to include in the request</param> /// <param name="fields">One or more fields to include in the request</param>
/// <returns>The list of fields for the <c>hx-params</c> attribute value</returns> /// <returns>The list of fields for the <tt>hx-params</tt> attribute value</returns>
let With fields = let With fields =
match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
/// <summary>Exclude the specified parameters</summary> /// <summary>Exclude the specified parameters</summary>
/// <param name="fields">One or more fields to exclude from the request</param> /// <param name="fields">One or more fields to exclude from the request</param>
/// <returns>The list of fields for the <c>hx-params</c> attribute value prefixed with "not"</returns> /// <returns>The list of fields for the <tt>hx-params</tt> attribute value prefixed with "not"</returns>
let Except fields = let Except fields =
With fields |> sprintf "not %s" With fields |> sprintf "not %s"
/// <summary>Helpers to define <c>hx-request</c> attribute values</summary> /// <summary>Helpers to define <tt>hx-request</tt> attribute values</summary>
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxRequest = module HxRequest =
@@ -257,60 +251,21 @@ module HxRequest =
$"\"timeout\": {ms}" $"\"timeout\": {ms}"
/// <summary>Include or exclude credentials from the request</summary> /// <summary>Include or exclude credentials from the request</summary>
/// <param name="send"><c>true</c> if credentials should be sent, <c>false</c> if not</param> /// <param name="send"><tt>true</tt> if credentials should be sent, <tt>false</tt> if not</param>
/// <returns>A string with the configured credential options</returns> /// <returns>A string with the configured credential options</returns>
let Credentials send = let Credentials send =
(toLowerBool >> sprintf "\"credentials\": %s") send (toLowerBool >> sprintf "\"credentials\": %s") send
/// <summary>Exclude or include headers from the request</summary> /// <summary>Exclude or include headers from the request</summary>
/// <param name="exclude"> /// <param name="exclude">
/// <c>true</c> if no headers should be sent; <c>false</c> if headers should be sent /// <tt>true</tt> if no headers should be sent; <tt>false</tt> if headers should be sent
/// </param> /// </param>
/// <returns>A string with the configured header options</returns> /// <returns>A string with the configured header options</returns>
let NoHeaders exclude = let NoHeaders exclude =
(toLowerBool >> sprintf "\"noHeaders\": %s") exclude (toLowerBool >> sprintf "\"noHeaders\": %s") exclude
/// <summary>Helpers for the <c>hx-sync</c> attribute</summary> /// Helpers for the `hx-trigger` attribute
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxSync =
/// <summary>Drop (ignore) this request if a request is already in flight</summary>
/// <remarks>This is the default for <c>hx-sync</c></remarks>
[<Literal>]
let Drop = "drop"
/// <summary>
/// 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
/// </summary>
[<Literal>]
let Abort = "abort"
/// <summary>Abort any current in-flight request and replace it with this one</summary>
[<Literal>]
let Replace = "replace"
/// <summary>Place this request in an element-associated queue</summary>
[<Literal>]
let Queue = "queue"
/// <summary>Queue only the first request received while another request is in flight</summary>
[<Literal>]
let QueueFirst = "queue first"
/// <summary>Queue only the last request received while another request is in flight</summary>
[<Literal>]
let QueueLast = "queue last"
/// <summary>Queue all requests received while another request is in flight</summary>
[<Literal>]
let QueueAll = "queue all"
/// <summary>Helpers for the <c>hx-trigger</c> attribute</summary>
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxTrigger = module HxTrigger =
@@ -322,164 +277,96 @@ module HxTrigger =
$"{parts[0]}[{parts[1]}&&{filter}]" $"{parts[0]}[{parts[1]}&&{filter}]"
| false -> $"{trigger}[{filter}]" | false -> $"{trigger}[{filter}]"
/// <summary>Trigger the event on a click</summary> /// Trigger the event on a click
[<Literal>]
let Click = "click" let Click = "click"
/// <summary>Trigger the event on page load</summary> /// Trigger the event on page load
[<Literal>]
let Load = "load" let Load = "load"
/// <summary>Trigger the event when the item is visible</summary> /// Trigger the event when the item is visible
[<Literal>]
let Revealed = "revealed" let Revealed = "revealed"
/// <summary>Trigger this event every [timing declaration]</summary> /// Trigger this event every [timing declaration]
/// <param name="duration">The duration on which this trigger should fire (e.g., "1s", "500ms")</param> let Every (duration : string) = $"every {duration}"
/// <returns>A trigger timing specification</returns>
let Every duration =
$"every %s{duration}"
/// <summary>Helpers for defining filters</summary> /// Helpers for defining filters
module Filter = module Filter =
/// <summary>Only trigger the event if the <c>ALT</c> key is pressed</summary> /// Only trigger the event if the `ALT` key is pressed
let Alt = appendFilter "altKey" let Alt = appendFilter "altKey"
/// <summary>Only trigger the event if the <c>CTRL</c> key is pressed</summary> /// Only trigger the event if the `CTRL` key is pressed
let Ctrl = appendFilter "ctrlKey" let Ctrl = appendFilter "ctrlKey"
/// <summary>Only trigger the event if the <c>SHIFT</c> key is pressed</summary> /// Only trigger the event if the `SHIFT` key is pressed
let Shift = appendFilter "shiftKey" let Shift = appendFilter "shiftKey"
/// <summary>Only trigger the event if <c>CTRL</c> and <c>ALT</c> are pressed</summary> /// Only trigger the event if `CTRL+ALT` are pressed
let CtrlAlt = Ctrl >> Alt let CtrlAlt = Ctrl >> Alt
/// <summary>Only trigger the event if <c>CTRL</c> and <c>SHIFT</c> are pressed</summary> /// Only trigger the event if `CTRL+SHIFT` are pressed
let CtrlShift = Ctrl >> Shift let CtrlShift = Ctrl >> Shift
/// <summary>Only trigger the event if <c>CTRL</c>, <c>ALT</c>, and <c>SHIFT</c> are pressed</summary> /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed
let CtrlAltShift = CtrlAlt >> Shift let CtrlAltShift = CtrlAlt >> Shift
/// <summary>Only trigger the event if <c>ALT</c> and <c>SHIFT</c> are pressed</summary> /// Only trigger the event if `ALT+SHIFT` are pressed
let AltShift = Alt >> Shift let AltShift = Alt >> Shift
/// Append a modifier to the current trigger /// Append a modifier to the current trigger
let private appendModifier modifier current = let private appendModifier modifier current =
if current = "" then modifier else $"{current} {modifier}" if current = "" then modifier else $"{current} {modifier}"
/// <summary>Only trigger once</summary> /// Only trigger once
/// <param name="action">The action which should only be fired once</param> let Once = appendModifier "once"
/// <returns>A trigger spec to fire the given action once</returns>
let Once action =
appendModifier "once" action
/// <summary>Trigger when changed</summary> /// Trigger when changed
/// <param name="elt">The element from which the <c>onchange</c> event will be emitted</param> let Changed = appendModifier "changed"
/// <returns>A trigger spec to fire when the given element changes</returns>
let Changed elt =
appendModifier "changed" elt
/// <summary>Delay execution; resets every time the event is seen</summary> /// Delay execution; resets every time the event is seen
/// <param name="duration">The duration for the delay (e.g., "1s", "500ms")</param> let Delay = sprintf "delay:%s" >> appendModifier
/// <param name="action">The action which should be fired after the given delay</param>
/// <returns>A trigger spec to fire the given action after the specified delay</returns>
let Delay duration action =
appendModifier $"delay:%s{duration}" action
/// <summary>Throttle execution; ignore other events, fire when duration passes</summary> /// Throttle execution; ignore other events, fire when duration passes
/// <param name="duration">The duration for the throttling (e.g., "1s", "500ms")</param> let Throttle = sprintf "throttle:%s" >> appendModifier
/// <param name="action">The action which should be fired after the given duration</param>
/// <returns>A trigger spec to fire the given action after the specified duration</returns>
let Throttle duration action =
appendModifier $"throttle:%s{duration}" action
/// <summary>Trigger this event from a CSS selector</summary> /// Trigger this event from a CSS selector
/// <param name="selector">A CSS selector to identify elements which may fire this trigger</param> let From = sprintf "from:%s" >> appendModifier
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the given element(s)</returns>
let From selector action =
appendModifier $"from:%s{selector}" action
/// <summary>Trigger this event from the <c>document</c> object</summary> /// Trigger this event from the `document` object
/// <param name="action">The action to be fired</param> let FromDocument = From "document"
/// <returns>A trigger spec to fire from the <c>document</c> element</returns>
let FromDocument action =
From "document" action
/// <summary>Trigger this event from the <c>window</c> object</summary> /// Trigger this event from the `window` object
/// <param name="action">The action to be fired</param> let FromWindow = From "window"
/// <returns>A trigger spec to fire from the <c>window</c> object</returns>
let FromWindow action =
From "window" action
/// <summary>Trigger this event from the closest parent CSS selector</summary> /// Trigger this event from the closest parent CSS selector
/// <param name="selector">The CSS selector from which the action should be fired</param> let FromClosest = sprintf "closest %s" >> From
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest element</returns>
let FromClosest selector action =
From $"closest %s{selector}" action
/// <summary>Trigger this event from the closest child CSS selector</summary> /// Trigger this event from the closest child CSS selector
/// <param name="selector">The child CSS selector from which the action should be fired</param> let FromFind = sprintf "find %s" >> From
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest child element</returns>
let FromFind selector action =
From $"find %s{selector}" action
/// <summary>Target the given CSS selector with the results of this event</summary> /// Target the given CSS selector with the results of this event
/// <param name="selector">The CSS selector to which the result of the action will be applied</param> let Target = sprintf "target:%s" >> appendModifier
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to target the given selector</returns>
let Target selector action =
appendModifier $"target:%s{selector}" action
/// <summary>Prevent any further events from occurring after this one fires</summary> /// Prevent any further events from occurring after this one fires
/// <param name="action">The action to be fired</param> let Consume = appendModifier "consume"
/// <returns>A trigger spec to fire the given action and prevent further events</returns>
let Consume action =
appendModifier "consume" action
/// <summary> /// Configure queueing when events fire when others are in flight; if unspecified, the default is "last"
/// Configure queueing when events fire when others are in flight; if unspecified, the default is <c>last</c> let Queue = sprintf "queue:%s" >> appendModifier
/// </summary>
/// <param name="how">
/// How the request should be queued (consider <see cref="QueueFirst" />, <see cref="QueueLast" />,
/// <see cref="QueueAll" />, and <see cref="QueueNone" />)
/// </param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let Queue how action =
let qSpec = if how = "" then "" else $":{how}"
appendModifier $"queue{qSpec}" action
/// <summary>Queue the first event, discard all others (i.e., a FIFO queue of 1)</summary> /// Queue the first event, discard all others (i.e., a FIFO queue of 1)
/// <param name="action">The action to be fired</param> let QueueFirst = Queue "first"
/// <returns>A trigger spec to queue the given action</returns>
let QueueFirst action =
Queue "first" action
/// <summary>Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)</summary> /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
/// <param name="action">The action to be fired</param> let QueueLast = Queue "last"
/// <returns>A trigger spec to queue the given action</returns>
let QueueLast action =
Queue "last" action
/// <summary>Queue all events; discard none</summary> /// Queue all events; discard none
/// <param name="action">The action to be fired</param> let QueueAll = Queue "all"
/// <returns>A trigger spec to queue the given action</returns>
let QueueAll action =
Queue "all" action
/// <summary>Queue no events; discard all</summary> /// Queue no events; discard all
/// <param name="action">The action to be fired</param> let QueueNone = Queue "none"
/// <returns>A trigger spec to queue the given action</returns>
let QueueNone action =
Queue "none" action
/// <summary>Helper to create the <c>hx-vals</c> attribute</summary> /// <summary>Helper to create the `hx-vals` attribute</summary>
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxVals = module HxVals =
@@ -487,341 +374,173 @@ module HxVals =
let From = Giraffe.Htmx.Common.toJson let From = Giraffe.Htmx.Common.toJson
open Giraffe.Htmx
/// <summary>Attributes and flags for htmx</summary> /// <summary>Attributes and flags for htmx</summary>
[<AutoOpen>] [<AutoOpen>]
module HtmxAttrs = module HtmxAttrs =
/// <summary>Progressively enhances anchors and forms to use AJAX requests</summary> /// <summary>Progressively enhances anchors and forms to use AJAX requests</summary>
/// <remarks>Use <c>_hxNoBoost</c> to set to false</remarks> /// <remarks>Use <tt>_hxNoBoost</tt> to set to false</remarks>
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
let _hxBoost = attr "hx-boost" "true" let _hxBoost = attr "hx-boost" "true"
/// <summary>Shows a <c>confirm()</c> dialog before issuing a request</summary> /// <summary>Shows a <tt>confirm()</tt> dialog before issuing a request</summary>
/// <param name="prompt">The prompt to present to the user when seeking their confirmation</param> /// <param name="prompt">The prompt to present to the user when seeking their confirmation</param>
/// <returns>A configured <c>hx-confirm</c> attribute</returns> /// <returns>A configured <tt>hx-confirm</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-confirm/">Documentation</seealso>
let _hxConfirm prompt = let _hxConfirm prompt =
attr "hx-confirm" prompt attr "hx-confirm" prompt
/// <summary>Issues a <c>DELETE</c> to the specified URL</summary> /// <summary>Issues a <tt>DELETE</tt> to the specified URL</summary>
/// <param name="url">The URL to which the <c>DELETE</c> request should be sent</param> /// <param name="url">The URL to which the <tt>DELETE</tt> request should be sent</param>
/// <returns>A configured <c>hx-delete</c> attribute</returns> /// <returns>A configured <tt>hx-delete</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-delete/">Documentation</seealso>
let _hxDelete url = let _hxDelete url =
attr "hx-delete" url attr "hx-delete" url
/// <summary>Disables htmx processing for the given node and any children nodes</summary> /// <summary>Disables htmx processing for the given node and any children nodes</summary>
/// <seealso href="https://htmx.org/attributes/hx-disable/">Documentation</seealso>
let _hxDisable = flag "hx-disable" let _hxDisable = flag "hx-disable"
/// <summary>Specifies elements that should be disabled when an htmx request is in flight</summary> /// <summary>Specifies elements that should be disabled when an htmx request is in flight</summary>
/// <param name="elt">The element to disable when an htmx request is in flight</param> /// <param name="elt">The element to disable when an htmx request is in flight</param>
/// <returns>A configured <c>hx-disabled-elt</c> attribute</returns> /// <returns>A configured <tt>hx-disabled-elt</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-disabled-elt/">Documentation</seealso>
let _hxDisabledElt elt = let _hxDisabledElt elt =
attr "hx-disabled-elt" elt attr "hx-disabled-elt" elt
/// <summary>Disinherit all ("*") or specific htmx attributes</summary> /// <summary>Disinherit all ("*") or specific htmx attributes</summary>
/// <param name="hxAttrs">The htmx attributes to disinherit (should start with "hx-")</param> /// <param name="hxAttrs">The htmx attributes to disinherit (should start with "hx-")</param>
/// <returns>A configured <c>hx-disinherit</c> attribute</returns> /// <returns>A configured <tt>hx-disinherit</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-disinherit/">Documentation</seealso>
let _hxDisinherit hxAttrs = let _hxDisinherit hxAttrs =
attr "hx-disinherit" hxAttrs attr "hx-disinherit" hxAttrs
/// <summary>Changes the request encoding type</summary> /// <summary>Changes the request encoding type</summary>
/// <param name="enc">The encoding type (use <c>HxEncoding</c> constants)</param> /// <param name="enc">The encoding type (use <tt>HxEncoding</tt> constants)</param>
/// <returns>A configured <c>hx-encoding</c> attribute</returns> /// <returns>A configured <tt>hx-encoding</tt> attribute</returns>
/// <seealso cref="HxEncoding" />
/// <seealso href="https://htmx.org/attributes/hx-encoding/">Documentation</seealso>
let _hxEncoding enc = let _hxEncoding enc =
attr "hx-encoding" enc attr "hx-encoding" enc
/// <summary>Extensions to use for this element</summary> /// <summary>Extensions to use for this element</summary>
/// <param name="exts">A list of extensions to apply to this element</param> /// <param name="exts">A list of extensions to apply to this element</param>
/// <returns>A configured <c>hx-ext</c> attribute</returns> /// <returns>A configured <tt>hx-ext</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-ext/">Documentation</seealso>
let _hxExt exts = let _hxExt exts =
attr "hx-ext" exts attr "hx-ext" exts
/// <summary>Issues a <c>GET</c> to the specified URL</summary> /// <summary>Issues a <tt>GET</tt> to the specified URL</summary>
/// <param name="url">The URL to which the <c>GET</c> request should be sent</param> /// <param name="url">The URL to which the <tt>GET</tt> request should be sent</param>
/// <returns>A configured <c>hx-get</c> attribute</returns> /// <returns>A configured <tt>hx-get</tt> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-get/">Documentation</seealso>
let _hxGet url = let _hxGet url =
attr "hx-get" url attr "hx-get" url
/// <summary>Adds to the headers that will be submitted with the request</summary> /// <summary>Adds to the headers that will be submitted with the request</summary>
/// <param name="hdrs">The headers to include with the request</param> [<System.Obsolete "Convert this parameter">]
/// <returns>A configured <c>hx-headers</c> attribute</returns> let _hxHeaders = attr "hx-headers"
/// <seealso href="https://htmx.org/attributes/hx-headers/">Documentation</seealso>
let _hxHeaders hdrs =
attr "hx-headers" hdrs
/// <summary>
/// Set to "false" to prevent pages with sensitive information from being stored in the history cache /// Set to "false" to prevent pages with sensitive information from being stored in the history cache
/// </summary> let _hxHistory = attr "hx-history"
/// <param name="shouldStore">Whether the page should be stored in the history cache</param>
/// <returns>A configured <c>hx-history</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-history/">Documentation</seealso>
let _hxHistory shouldStore =
attr "hx-history" (toLowerBool shouldStore)
/// <summary>The element to snapshot and restore during history navigation</summary> /// The element to snapshot and restore during history navigation
/// <seealso href="https://htmx.org/attributes/hx-history-elt/">Documentation</seealso> let _hxHistoryElt = flag "hx-history-elt"
let _hxHistoryElt =
flag "hx-history-elt"
/// <summary>Includes additional data in AJAX requests</summary> /// Includes additional data in AJAX requests
/// <param name="spec">The specification of what should be included in the request</param> let _hxInclude = attr "hx-include"
/// <returns>A configured <c>hx-include</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-include/">Documentation</seealso>
let _hxInclude spec =
attr "hx-include" spec
/// <summary>The element to put the <c>htmx-request</c> class on during the AJAX request</summary> /// The element to put the htmx-request class on during the AJAX request
/// <param name="selector">The selector for the indicator element</param> let _hxIndicator = attr "hx-indicator"
/// <returns>A configured <c>hx-indicator</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-indicator/">Documentation</seealso>
let _hxIndicator selector =
attr "hx-indicator" selector
/// <summary>Overrides a previous <c>hx-boost</c> (hx-boost="false")</summary> /// Overrides a previous `hx-boost`
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso> let _hxNoBoost = attr "hx-boost" "false"
let _hxNoBoost =
attr "hx-boost" "false"
/// <summary>Attach an event handler for DOM events</summary> /// Attach an event handler for DOM events
/// <param name="evtName">The name of the event</param> let _hxOnEvent evtName =
/// <param name="handler">The script to be executed when the event occurs</param> attr $"hx-on:%s{evtName}"
/// <returns>A configured <c>hx-on</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
let _hxOnEvent evtName handler =
attr $"hx-on:%s{evtName}" handler
/// <summary>Attach an event handler for htmx events</summary> /// Attach an event handler for htmx events
/// <param name="hxEvent">The <c>HxEvent</c> to be handled</param> let _hxOnHxEvent (hxEvent: HxEvent) =
/// <param name="handler">The script to be executed when the event occurs</param> _hxOnEvent $":{hxEvent.ToHxOnString()}"
/// <returns>A configured <c>hx-on::</c> attribute</returns>
/// <seealso cref="HxEvent" />
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
let _hxOnHxEvent (hxEvent: HxEvent) handler =
_hxOnEvent $":{hxEvent.ToHxOnString()}" handler
/// <summary>Filters the parameters that will be submitted with a request</summary> /// Filters the parameters that will be submitted with a request
/// <param name="toInclude">The fields to include (use <c>HxParams</c> to generate this value)</param> let _hxParams = attr "hx-params"
/// <returns>A configured <c>hx-params</c> attribute</returns>
/// <seealso cref="HxParams" />
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
let _hxParams toInclude =
attr "hx-params" toInclude
/// <summary>Issues a <c>PATCH</c> to the specified URL</summary> /// Issues a PATCH to the specified URL
/// <param name="url">The URL to which the request should be directed</param> let _hxPatch = attr "hx-patch"
/// <returns>A configured <c>hx-patch</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-patch/">Documentation</seealso>
let _hxPatch url =
attr "hx-patch" url
/// <summary>Issues a <c>POST</c> to the specified URL</summary> /// Issues a POST to the specified URL
/// <param name="url">The URL to which the request should be directed</param> let _hxPost = attr "hx-post"
/// <returns>A configured <c>hx-post</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-post/">Documentation</seealso>
let _hxPost url =
attr "hx-post" url
/// <summary>Preserves an element between requests</summary> /// Preserves an element between requests
/// <seealso href="https://htmx.org/attributes/hx-preserve/">Documentation</seealso> let _hxPreserve = attr "hx-preserve" "true"
let _hxPreserve =
flag "hx-preserve"
/// <summary>Shows a <c>prompt()</c> dialog before submitting a request</summary> /// Shows a prompt before submitting a request
/// <param name="text">The text for the prompt</param> let _hxPrompt = attr "hx-prompt"
/// <returns>A configured <c>hx-prompt</c> attribute</returns>
/// <remarks>The value provided will be in the <c>HX-Prompt</c> request header</remarks>
/// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso>
let _hxPrompt text =
attr "hx-prompt" text
/// <summary>Pushes the URL into the location bar, creating a new history entry</summary> /// Pushes the URL into the location bar, creating a new history entry
/// <param name="spec"> let _hxPushUrl = attr "hx-push-url"
/// <ul>
/// <li>"true" to push the fetched URL</li>
/// <li>"false" to explicitly not push the fetched URL</li>
/// <li>A specific URL to push</li>
/// </ul>
/// </param>
/// <returns>A configured <c>hx-push-url</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-push-url/">Documentation</seealso>
let _hxPushUrl spec =
attr "hx-push-url" spec
/// <summary>Issues a <c>PUT</c> to the specified URL</summary> /// Issues a PUT to the specified URL
/// <param name="url">The URL to which the request should be directed</param> let _hxPut = attr "hx-put"
/// <returns>A configured <c>hx-put</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-put/">Documentation</seealso>
let _hxPut url =
attr "hx-put" url
/// <summary>Replaces the current URL in the browser's history stack</summary> /// Replaces the current URL in the browser's history stack
/// <param name="spec"> let _hxReplaceUrl = attr "hx-replace-url"
/// <ul>
/// <li>"true" to replace the current URL with the fetched one</li>
/// <li>"false" to explicitly replace nothing</li>
/// <li>A specific URL to replace in the browser's history</li>
/// </ul>
/// </param>
/// <returns>A configured <c>hx-replace-url</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-replace-url/">Documentation</seealso>
let _hxReplaceUrl spec =
attr "hx-replace-url" spec
/// <summary>Configures various aspects of the request</summary> /// Configures various aspects of the request
/// <param name="spec">The configuration spec (use <c>HxRequest.Configure</c> to create value)</param> let _hxRequest = attr "hx-request"
/// <returns>A configured <c>hx-request</c> attribute</returns>
/// <seealso cref="HxRequest.Configure" />
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
let _hxRequest spec =
attr "hx-request" spec
/// <summary>Selects a subset of the server response to process</summary> /// Selects a subset of the server response to process
/// <param name="selector">A CSS selector for the content to be selected</param> let _hxSelect = attr "hx-select"
/// <returns>A configured <c>hx-select</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-select/">Documentation</seealso>
let _hxSelect selector =
attr "hx-select" selector
/// <summary>Selects a subset of an out-of-band server response</summary> /// Selects a subset of an out-of-band server response
/// <param name="selectors">One or more comma-delimited CSS selectors for the content to be selected</param> let _hxSelectOob = attr "hx-select-oob"
/// <returns>A configured <c>hx-select-oob</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-select-oob/">Documentation</seealso>
let _hxSelectOob selectors =
attr "hx-select-oob" selectors
/// <summary> /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd')
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>) let _hxSwap = attr "hx-swap"
/// </summary>
/// <param name="swap">The type of swap to perform (use <c>HxSwap</c> values)</param>
/// <returns>A configured <c>hx-swap</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
let _hxSwap swap =
attr "hx-swap" swap
/// <summary> /// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd'), enabling CSS
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>), enabling /// transitions
/// CSS transitions let _hxSwapWithTransition = sprintf "%s transition:true" >> _hxSwap
/// </summary>
/// <param name="swap">The type of swap to perform (use <c>HxSwap</c> values)</param>
/// <returns>A configured <c>hx-swap</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
let _hxSwapWithTransition swap =
_hxSwap $"%s{swap} transition:true"
/// <summary>
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
/// </summary> let _hxSwapOob = attr "hx-swap-oob"
/// <param name="swap">
/// <ul>
/// <li>"true" to mark this as an OOB swap</li>
/// <li>Any <c>HxSwap</c> value</li>
/// <li>Any <c>HxSwap</c> value, followed by a colon (<c>:</c>) and a CSS selector</li>
/// </ul>
/// </param>
/// <returns>A configured <c>hx-swap-oob</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-swap-oob/">Documentation</seealso>
let _hxSwapOob swap =
attr "hx-swap-oob" swap
/// <summary>Synchronize events based on another element</summary> /// Synchronize events based on another element
/// <param name="selector">A CSS selector for the element with which this one should sync</param> let _hxSync = attr "hx-sync"
/// <param name="action">The request synchronization action to perform (use <c>HxSync</c> values)</param>
/// <returns>A configured <c>hx-sync</c> attribute</returns>
/// <seealso cref="HxSync" />
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
let _hxSync selector action =
attr "hx-sync" $"%s{selector}:%s{action}"
/// <summary>Specifies the target element to be swapped</summary> /// Specifies the target element to be swapped
/// <param name="selector">A CSS selector or relative reference (or both) to identify the target</param> let _hxTarget = attr "hx-target"
/// <returns>A configured <c>hx-target</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-target/">Documentation</seealso>
let _hxTarget selector =
attr "hx-target" selector
/// <summary>Specifies the event that triggers the request</summary> /// Specifies the event that triggers the request
/// <param name="spec">The trigger specification (use <c>HxTrigger</c> to create)</param> let _hxTrigger = attr "hx-trigger"
/// <returns>A configured <c>hx-trigger</c> attribute</returns>
/// <seealso cref="HxTrigger" />
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
let _hxTrigger spec =
attr "hx-trigger" spec
/// <summary>Validate an input element (uses HTML5 validation API)</summary> /// Validate an input element (uses HTML5 validation API)
/// <seealso href="https://htmx.org/attributes/hx-validate/">Documentation</seealso> let _hxValidate = flag "hx-validate"
let _hxValidate =
flag "hx-validate"
/// <summary>Adds to the parameters that will be submitted with the request</summary> /// Adds to the parameters that will be submitted with the request
/// <param name="values">The values for the parameters (use <c>HxVals.From</c> to create)</param> let _hxVals = attr "hx-vals"
/// <returns>A configured <c>hx-vals</c> attribute</returns>
/// <seealso cref="HxVals.From" />
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
let _hxVals values =
attr "hx-vals" values
/// <summary>The URL of the SSE server</summary> /// The name of the message to swap into the DOM.
/// <param name="url">The URL from which events will be received</param> let _sseSwap = attr "sse-swap"
/// <returns>A configured <c>sse-connect</c> attribute</returns>
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
let _sseConnect url =
attr "sse-connect" url
/// <summary>The name(s) of the message(s) to swap into the DOM</summary> /// The URL of the SSE server.
/// <param name="messages">The message names (comma-delimited) to swap (use "message" for unnamed events)</param> let _sseConnect = attr "sse-connect"
/// <returns>A configured <c>sse-swap</c> attribute</returns>
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
let _sseSwap messages =
attr "sse-swap" messages
/// <summary>Script tags to pull htmx into a web page</summary> /// Script tags to pull htmx into a web page
module Script = module Script =
open System /// Script tag to load the minified version from unpkg.com
let minified =
/// <summary>Script tag to load the package-provided version of htmx</summary> script [ _src "https://unpkg.com/htmx.org@2.0.4"
let local = script [ _src htmxLocalScript ] [] _integrity "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnMinified =
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"
_integrity "sha256-Iig+9oy3VFkU8KiKG97cclanA9HVgMHSVSF9ClDTExM="
_crossorigin "anonymous" ] [] _crossorigin "anonymous" ] []
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary> /// Script tag to load the unminified version from unpkg.com
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks> let unminified =
let cdnUnminified = script [ _src "https://unpkg.com/htmx.org@2.0.4/dist/htmx.js"
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.js" _integrity "sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1"
_integrity "sha256-upUwYnay6R+DA68rROTAP+EdfO3NvOqtE513PgDyAYM="
_crossorigin "anonymous" ] [] _crossorigin "anonymous" ] []
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
[<Obsolete "Use cdnMinified instead; this will be removed in v4">]
let minified = cdnMinified
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary> /// Functions to extract and render an HTML fragment from a document
[<Obsolete "Use cdnUnminified instead; this will be removed in v4">]
let unminified = cdnUnminified
/// <summary>Functions to extract and render an HTML fragment from a document</summary>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module RenderFragment = module RenderFragment =
/// Does this element have an ID matching the requested ID name? /// 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 snd elt
|> Array.exists (fun attr -> |> Array.exists (fun attr ->
match attr with match attr with
@@ -829,83 +548,62 @@ module RenderFragment =
| Boolean _ -> false) | Boolean _ -> false)
/// Generate a message if the requested ID node is not found /// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId: string) = let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>" $"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// <summary>Find the node with the named ID</summary> /// Find the node with the named ID
/// <param name="nodeId">The <c>id</c> attribute to find</param> let rec findIdNode nodeId (node : XmlNode) : XmlNode option =
/// <param name="node">The node tree to search</param>
/// <returns>The node with the requested <c>id</c> attribute, or <c>None</c> if it was not found</returns>
let rec findIdNode nodeId (node: XmlNode) : XmlNode option =
match node with match node with
| Text _ -> None | Text _ -> None
| VoidElement elt -> if isIdElement nodeId elt then Some node else None | VoidElement elt -> if isIdElement nodeId elt then Some node else None
| ParentNode (elt, children) -> | ParentNode (elt, children) ->
if isIdElement nodeId elt then Some node else children |> List.tryPick (findIdNode nodeId) if isIdElement nodeId elt then Some node else children |> List.tryPick (fun c -> findIdNode nodeId c)
/// <summary>Functions to render a fragment as a string</summary> /// Functions to render a fragment as a string
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module AsString = module AsString =
/// <summary>Render to HTML for the given ID</summary> /// Render to HTML for the given ID
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param> let htmlFromNodes nodeId (nodes : XmlNode list) =
/// <param name="nodes">The node trees to search</param> match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
/// <returns>The HTML for the given <c>id</c> node, or an error message if it was not found</returns>
let htmlFromNodes nodeId (nodes: XmlNode list) =
match nodes |> List.tryPick (findIdNode nodeId) with
| Some idNode -> RenderView.AsString.htmlNode idNode | Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId | None -> nodeNotFound nodeId
/// <summary>Render to HTML for the given ID</summary> /// Render to HTML for the given ID
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
/// <param name="node">The node tree to search</param>
/// <returns>The HTML for the given <c>id</c> node, or an error message if it was not found</returns>
let htmlFromNode nodeId node = let htmlFromNode nodeId node =
match findIdNode nodeId node with match findIdNode nodeId node with
| Some idNode -> RenderView.AsString.htmlNode idNode | Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId | None -> nodeNotFound nodeId
/// <summary>Functions to render a fragment as bytes</summary> /// Functions to render a fragment as bytes
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module AsBytes = module AsBytes =
let private utf8 = System.Text.Encoding.UTF8 let private utf8 = System.Text.Encoding.UTF8
/// <summary>Render to bytes for the given ID</summary> /// Render to HTML for the given ID
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param> let htmlFromNodes nodeId (nodes : XmlNode list) =
/// <param name="nodes">The node trees to search</param> match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
/// <returns>The bytes for the given <c>id</c> node, or an error message if it was not found</returns>
let htmlFromNodes nodeId (nodes: XmlNode list) =
match nodes |> List.tryPick (findIdNode nodeId) with
| Some idNode -> RenderView.AsBytes.htmlNode idNode | Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes | None -> nodeNotFound nodeId |> utf8.GetBytes
/// <summary>Render to bytes for the given ID</summary> /// Render to HTML for the given ID
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
/// <param name="node">The node tree to search</param>
/// <returns>The bytes for the given <c>id</c> node, or an error message if it was not found</returns>
let htmlFromNode nodeId node = let htmlFromNode nodeId node =
match findIdNode nodeId node with match findIdNode nodeId node with
| Some idNode -> RenderView.AsBytes.htmlNode idNode | Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes | None -> nodeNotFound nodeId |> utf8.GetBytes
/// <summary>Functions to render a fragment into a StringBuilder</summary> /// Functions to render a fragment into a StringBuilder
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module IntoStringBuilder = module IntoStringBuilder =
/// <summary>Render HTML into a <c>StringBuilder</c> for the given ID</summary> /// Render to HTML for the given ID
/// <param name="sb">The <c>StringBuilder</c> into which the bytes will be rendered</param> let htmlFromNodes sb nodeId (nodes : XmlNode list) =
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param> match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
/// <param name="nodes">The node trees to search</param>
let htmlFromNodes sb nodeId (nodes: XmlNode list) =
match nodes |> List.tryPick (findIdNode nodeId) with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore | None -> nodeNotFound nodeId |> sb.Append |> ignore
/// <summary>Render HTML into a <c>StringBuilder</c> for the given ID</summary> /// Render to HTML for the given ID
/// <param name="sb">The <c>StringBuilder</c> into which the bytes will be rendered</param>
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
/// <param name="node">The node tree to search</param>
let htmlFromNode sb nodeId node = let htmlFromNode sb nodeId node =
match findIdNode nodeId node with match findIdNode nodeId node with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode

View File

@@ -2,7 +2,7 @@
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine. This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
**htmx version: 2.0.8** **htmx version: 2.0.4**
_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_ _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` - `HxTrigger`
- `HxVals` - `HxVals`
`Htmx.Script.local` creates an `XmlNode` to load the package-provided htmx library. There are also two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.cdnMinified` loads the minified version, and `Htmx.Script.cdnUnminified` loads the unminified version (useful for debugging). When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`. There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging).
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute). 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).