Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e28714215 | |||
| 28cec1cb70 |
@@ -6,8 +6,6 @@
|
||||
|
||||
htmx uses attributes and HTTP headers to attain its interactivity; the libraries here contain extensions to both Giraffe and Giraffe View Engine to enable strongly-typed development of htmx applications.
|
||||
|
||||
> **NOTE** htmx v4 is curently in beta; this library is supporting it on the [`htmx-v4` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v4/). Beta versions of the v4 library include warnings about obsolete constructs. The htmx project intends to continue supporting the 2.x line (just as they still do the 1.x line, which we track on the [`htmx-v1` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v1/)); the upgrade is available, not forced. See the [htmx v4 migration docs](https://four.htmx.org/docs#migration) for more information.
|
||||
|
||||
## Installation
|
||||
|
||||
`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods.
|
||||
@@ -16,8 +14,6 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
|
||||
|---|---|
|
||||
|[](https://www.nuget.org/packages/Giraffe.Htmx/)|[](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`)
|
||||
|
||||
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.
|
||||
@@ -46,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.
|
||||
|
||||
`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`)
|
||||
|
||||
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.
|
||||
@@ -72,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
|
||||
|
||||
|
||||
+11
-25
@@ -2,12 +2,6 @@
|
||||
[<AutoOpen>]
|
||||
module Giraffe.Htmx.Common
|
||||
|
||||
/// <summary>The version of htmx embedded in the package</summary>
|
||||
let HtmxVersion = "2.0.10"
|
||||
|
||||
/// <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>
|
||||
/// <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>
|
||||
@@ -17,43 +11,35 @@ let internal toJson (pairs: (string * string) list) =
|
||||
|> String.concat ", "
|
||||
|> 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>
|
||||
/// <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) =
|
||||
(string boolValue).ToLowerInvariant()
|
||||
|
||||
|
||||
/// <summary>Valid values for the <c>hx-swap</c> attribute / <c>HX-Reswap</c> header</summary>
|
||||
/// <remarks>May be combined with <c>swap</c> / <c>settle</c> / <c>scroll</c> / <c>show</c> config)</remarks>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
|
||||
/// <summary>Valid values for the <tt>hx-swap</tt> attribute / <tt>HX-Reswap</tt> header</summary>
|
||||
/// <remarks>May be combined with <tt>swap</tt> / <tt>settle</tt> / <tt>scroll</tt> / <tt>show</tt> config)</remarks>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxSwap =
|
||||
|
||||
|
||||
/// <summary>The default, replace the inner HTML of the target element</summary>
|
||||
[<Literal>]
|
||||
let InnerHtml = "innerHTML"
|
||||
|
||||
|
||||
/// <summary>Replace the entire target element with the response</summary>
|
||||
[<Literal>]
|
||||
let OuterHtml = "outerHTML"
|
||||
|
||||
|
||||
/// <summary>Insert the response before the target element</summary>
|
||||
[<Literal>]
|
||||
let BeforeBegin = "beforebegin"
|
||||
|
||||
|
||||
/// <summary>Insert the response before the first child of the target element</summary>
|
||||
[<Literal>]
|
||||
let AfterBegin = "afterbegin"
|
||||
|
||||
|
||||
/// <summary>Insert the response after the last child of the target element</summary>
|
||||
[<Literal>]
|
||||
let BeforeEnd = "beforeend"
|
||||
|
||||
|
||||
/// <summary>Insert the response after the target element</summary>
|
||||
[<Literal>]
|
||||
let AfterEnd = "afterend"
|
||||
|
||||
|
||||
/// <summary>Does not append content from response (out of band items will still be processed).</summary>
|
||||
[<Literal>]
|
||||
let None = "none"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
## Giraffe.Htmx.Common
|
||||
|
||||
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. 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.10**
|
||||
**htmx version: 2.0.4**
|
||||
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
@@ -1,13 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<VersionPrefix>2.0.10</VersionPrefix>
|
||||
<TargetFrameworks>net8.0;net9.0</TargetFrameworks>
|
||||
<VersionPrefix>2.0.5</VersionPrefix>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageReleaseNotes>- Updates provided script and CDN links to serve htmx 2.0.10 (no header or attribute changes)
|
||||
|
||||
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>
|
||||
<PackageReleaseNotes>Add full packaged XML documentation; update script tags to pull htmx 2.0.5 (no header or attribute changes)</PackageReleaseNotes>
|
||||
<Authors>danieljsummers</Authors>
|
||||
<Company>Bit Badger Solutions</Company>
|
||||
<PackageProjectUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</PackageProjectUrl>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30114.105
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx", "Htmx\Giraffe.Htmx.fsproj", "{8AB3085C-5236-485A-8565-A09106E72E1E}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.ViewEngine.Htmx", "ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj", "{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx.Common", "Common\Giraffe.Htmx.Common.fsproj", "{75D66845-F93A-4463-AD29-A8B16E4D4BA9}"
|
||||
EndProject
|
||||
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{39823773-4311-4E79-9CA0-F9DDC40CAF6A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,6 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="Common/Giraffe.Htmx.Common.fsproj" />
|
||||
<Project Path="Htmx/Giraffe.Htmx.fsproj" />
|
||||
<Project Path="Tests/Tests.fsproj" />
|
||||
<Project Path="ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj" />
|
||||
</Solution>
|
||||
+35
-57
@@ -11,7 +11,7 @@ let private hdr (headers : IHeaderDictionary) hdr =
|
||||
/// Extensions to the header dictionary
|
||||
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
|
||||
with get () = hdr this "HX-Boosted" |> Option.map bool.Parse
|
||||
|
||||
@@ -19,27 +19,29 @@ type IHeaderDictionary with
|
||||
member this.HxCurrentUrl
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
with get () = hdr this "HX-Trigger-Name"
|
||||
|
||||
@@ -62,123 +64,99 @@ module Handlers =
|
||||
|
||||
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>
|
||||
/// <param name="url">The URL to be pushed</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> 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>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Push-Url</tt> header set</returns>
|
||||
let withHxPushUrl (url: string) : HttpHandler =
|
||||
setHttpHeader "HX-Push-Url" url
|
||||
|
||||
/// <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>
|
||||
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Push-Url</tt> header set to <tt>false</tt></returns>
|
||||
let withHxNoPushUrl : HttpHandler =
|
||||
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>
|
||||
/// <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>
|
||||
/// <seealso href="https://htmx.org/headers/hx-redirect/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Redirect</tt> header set</returns>
|
||||
let withHxRedirect (url: string) : HttpHandler =
|
||||
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>
|
||||
/// <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 =
|
||||
(toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh
|
||||
|
||||
/// <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>
|
||||
/// <returns>An HTTP handler with the <c>HX-Replace-URL</c> 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>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Replace-URL</tt> header set</returns>
|
||||
let withHxReplaceUrl url : HttpHandler =
|
||||
setHttpHeader "HX-Replace-Url" url
|
||||
|
||||
/// <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>
|
||||
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Replace-URL</tt> header set to <tt>false</tt></returns>
|
||||
let withHxNoReplaceUrl : HttpHandler =
|
||||
toLowerBool false |> withHxReplaceUrl
|
||||
|
||||
/// <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>
|
||||
/// <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 =
|
||||
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>
|
||||
/// <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>
|
||||
let withHxReswap (swap: string) : HttpHandler =
|
||||
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>
|
||||
/// <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 =
|
||||
setHttpHeader "HX-Retarget" target
|
||||
|
||||
/// <summary>Allows you to trigger a single client side event</summary>
|
||||
/// <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>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger</tt> header set</returns>
|
||||
let withHxTrigger (evt: string) : HttpHandler =
|
||||
setHttpHeader "HX-Trigger" evt
|
||||
|
||||
/// <summary>Allows you to trigger multiple client side events</summary>
|
||||
/// <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>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger</tt> header set for all given events</returns>
|
||||
let withHxTriggerMany evts : HttpHandler =
|
||||
toJson evts |> setHttpHeader "HX-Trigger"
|
||||
|
||||
/// <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>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger-After-Settle</tt> header set</returns>
|
||||
let withHxTriggerAfterSettle (evt: string) : HttpHandler =
|
||||
setHttpHeader "HX-Trigger-After-Settle" evt
|
||||
|
||||
/// <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>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set for all given events</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger-After-Settle</tt> header set for all given events</returns>
|
||||
let withHxTriggerManyAfterSettle evts : HttpHandler =
|
||||
toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
|
||||
|
||||
/// <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>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger-After-Swap</tt> header set</returns>
|
||||
let withHxTriggerAfterSwap (evt: string) : HttpHandler =
|
||||
setHttpHeader "HX-Trigger-After-Swap" evt
|
||||
|
||||
/// <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>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set for all given events</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
/// <returns>An HTTP handler with the <tt>HX-Trigger-After-Swap</tt> header set for all given events</returns>
|
||||
let withHxTriggerManyAfterSwap evts : HttpHandler =
|
||||
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>"""
|
||||
|
||||
+1
-3
@@ -2,7 +2,7 @@
|
||||
|
||||
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
|
||||
|
||||
**htmx version: 2.0.10**
|
||||
**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.*._
|
||||
|
||||
@@ -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.
|
||||
|
||||
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`.
|
||||
|
||||
### 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.
|
||||
+1
-7
@@ -3,12 +3,6 @@ module Common
|
||||
open Expecto
|
||||
open Giraffe.Htmx
|
||||
|
||||
/// Test to ensure the version was updated
|
||||
let version =
|
||||
test "HtmxVersion is correct" {
|
||||
Expect.equal HtmxVersion "2.0.10" "htmx version incorrect"
|
||||
}
|
||||
|
||||
/// Tests for the HxSwap module
|
||||
let swap =
|
||||
testList "HxSwap" [
|
||||
@@ -36,4 +30,4 @@ let swap =
|
||||
]
|
||||
|
||||
/// All tests for this module
|
||||
let allTests = testList "Htmx.Common" [ version; swap ]
|
||||
let allTests = testList "Htmx.Common" [ swap ]
|
||||
|
||||
+100
-120
@@ -3,7 +3,6 @@ module Htmx
|
||||
open System
|
||||
open Expecto
|
||||
open Giraffe.Htmx
|
||||
open Microsoft.AspNetCore.Html
|
||||
open Microsoft.AspNetCore.Http
|
||||
open NSubstitute
|
||||
|
||||
@@ -12,22 +11,22 @@ let dictExtensions =
|
||||
testList "IHeaderDictionaryExtensions" [
|
||||
testList "HxBoosted" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present and true" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Boosted", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Boosted", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
|
||||
Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true"
|
||||
}
|
||||
test "succeeds when the header is present and false" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Boosted", "false")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Boosted", "false")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
|
||||
Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false"
|
||||
@@ -35,14 +34,14 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxCurrentUrl" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Current-URL", "http://localhost/test.htm")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Current-URL", "http://localhost/test.htm")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present"
|
||||
Expect.equal
|
||||
@@ -52,22 +51,22 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxHistoryRestoreRequest" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present and true" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-History-Restore-Request", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-History-Restore-Request", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
|
||||
Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true"
|
||||
}
|
||||
test "succeeds when the header is present and false" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-History-Restore-Request", "false")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-History-Restore-Request", "false")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
|
||||
Expect.isFalse
|
||||
@@ -76,14 +75,14 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxPrompt" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Prompt", "of course")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Prompt", "of course")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect"
|
||||
@@ -91,22 +90,22 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxRequest" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present and true" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Request", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Request", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
|
||||
Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true"
|
||||
}
|
||||
test "succeeds when the header is present and false" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Request", "false")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Request", "false")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
|
||||
Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false"
|
||||
@@ -114,14 +113,14 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxTarget" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Target", "#leItem")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Target", "#leItem")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect"
|
||||
@@ -129,14 +128,14 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxTrigger" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Trigger", "#trig")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Trigger", "#trig")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect"
|
||||
@@ -144,14 +143,14 @@ let dictExtensions =
|
||||
]
|
||||
testList "HxTriggerName" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned"
|
||||
}
|
||||
test "HxTriggerName succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Trigger-Name", "click")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Trigger-Name", "click")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect"
|
||||
@@ -164,36 +163,36 @@ let reqExtensions =
|
||||
testList "HttpRequestExtensions" [
|
||||
testList "IsHtmx" [
|
||||
test "succeeds when request is not from htmx" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request"
|
||||
}
|
||||
}
|
||||
test "succeeds when request is from htmx" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Request", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Request", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request"
|
||||
}
|
||||
]
|
||||
testList "IsHtmxRefresh" [
|
||||
test "succeeds when request is not from htmx" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
|
||||
}
|
||||
test "succeeds when request is from htmx, but not a refresh" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Request", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Request", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
|
||||
}
|
||||
test "IsHtmxRefresh succeeds when request is from htmx and is a refresh" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Request", "true")
|
||||
dic.Add("HX-History-Restore-Request", "true")
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
dic.Add ("HX-Request", "true")
|
||||
dic.Add ("HX-History-Restore-Request", "true")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh"
|
||||
}
|
||||
@@ -203,38 +202,30 @@ let reqExtensions =
|
||||
open System.Threading.Tasks
|
||||
|
||||
/// Dummy "next" parameter to get the pipeline to execute/terminate
|
||||
let next (ctx: HttpContext) = Task.FromResult(Some ctx)
|
||||
|
||||
let next (ctx : HttpContext) = Task.FromResult (Some ctx)
|
||||
|
||||
/// Tests for the HttpHandler functions provided in the Handlers module
|
||||
let handlers =
|
||||
testList "HandlerTests" [
|
||||
testTask "withHxLocation succeeds" {
|
||||
let ctx = Substitute.For<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" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxPushUrl "/a-new-url" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
|
||||
Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect"
|
||||
}
|
||||
testTask "withHxNoPushUrl succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxNoPushUrl next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
|
||||
Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect"
|
||||
}
|
||||
testTask "withHxRedirect succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxRedirect "/somewhere-else" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present"
|
||||
@@ -242,16 +233,16 @@ let handlers =
|
||||
}
|
||||
testList "withHxRefresh" [
|
||||
testTask "succeeds when set to true" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxRefresh true next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
|
||||
Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect"
|
||||
}
|
||||
testTask "succeeds when set to false" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxRefresh false next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
|
||||
@@ -259,56 +250,56 @@ let handlers =
|
||||
}
|
||||
]
|
||||
testTask "withHxReplaceUrl succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
|
||||
Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect"
|
||||
}
|
||||
testTask "withHxNoReplaceUrl succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxNoReplaceUrl next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
|
||||
Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect"
|
||||
}
|
||||
testTask "withHxReselect succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxReselect "#test" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present"
|
||||
Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect"
|
||||
}
|
||||
}
|
||||
testTask "withHxReswap succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxReswap HxSwap.BeforeEnd next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present"
|
||||
Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect"
|
||||
}
|
||||
}
|
||||
testTask "withHxRetarget succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxRetarget "#somewhereElse" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present"
|
||||
Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect"
|
||||
}
|
||||
testTask "withHxTrigger succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTrigger "doSomething" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
|
||||
Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerMany succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
|
||||
@@ -316,8 +307,8 @@ let handlers =
|
||||
dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerAfterSettle succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
|
||||
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"
|
||||
}
|
||||
testTask "withHxTriggerManyAfterSettle succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
|
||||
Expect.isTrue
|
||||
@@ -336,16 +327,16 @@ let handlers =
|
||||
"The HX-Trigger-After-Settle value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerAfterSwap succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerAfterSwap "justASec" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present"
|
||||
Expect.equal dic["HX-Trigger-After-Swap"].[0] "justASec" "The HX-Trigger-After-Swap value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerManyAfterSwap succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
let ctx = Substitute.For<HttpContext> ()
|
||||
let dic = HeaderDictionary ()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present"
|
||||
@@ -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
|
||||
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script ]
|
||||
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers ]
|
||||
|
||||
+25
-65
@@ -406,8 +406,8 @@ let hxEvent =
|
||||
Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
|
||||
]
|
||||
|
||||
/// Tests for the HxHeaders module
|
||||
let hxHeaders =
|
||||
testList "HxHeaders" [
|
||||
@@ -497,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
|
||||
let hxTrigger =
|
||||
testList "HxTrigger" [
|
||||
@@ -610,7 +584,7 @@ let hxTrigger =
|
||||
testList "FromDocument" [
|
||||
test "succeeds when it is the first modifier" {
|
||||
Expect.equal (HxTrigger.FromDocument "") "from:document" "FromDocument modifier incorrect"
|
||||
}
|
||||
}
|
||||
test "succeeds when it is not the first modifier" {
|
||||
Expect.equal (HxTrigger.FromDocument "click") "click from:document" "FromDocument modifier incorrect"
|
||||
}
|
||||
@@ -662,9 +636,6 @@ let hxTrigger =
|
||||
test "succeeds when it is not the first modifier" {
|
||||
Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect"
|
||||
}
|
||||
test "succeeds when no type of queueing is given" {
|
||||
Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect"
|
||||
}
|
||||
]
|
||||
testList "QueueFirst" [
|
||||
test "succeeds when it is the first modifier" {
|
||||
@@ -715,7 +686,7 @@ let hxVals =
|
||||
]
|
||||
]
|
||||
|
||||
/// Pipe-able assertion for a rendered node
|
||||
/// Pipe-able assertion for a rendered node
|
||||
let shouldRender expected node =
|
||||
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
|
||||
|
||||
@@ -755,7 +726,7 @@ let attributes =
|
||||
|> shouldRender """<figure hx-headers="{ "X-Special-Header": "some-header" }"></figure>"""
|
||||
}
|
||||
test "_hxHistory succeeds" {
|
||||
span [ _hxHistory false ] [] |> shouldRender """<span hx-history="false"></span>"""
|
||||
span [ _hxHistory "false" ] [] |> shouldRender """<span hx-history="false"></span>"""
|
||||
}
|
||||
test "_hxHistoryElt succeeds" {
|
||||
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">"""
|
||||
}
|
||||
test "_hxPreserve succeeds" {
|
||||
img [ _hxPreserve ] |> shouldRender """<img hx-preserve>"""
|
||||
img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">"""
|
||||
}
|
||||
test "_hxPrompt succeeds" {
|
||||
strong [ _hxPrompt "Who goes there?" ] []
|
||||
@@ -821,8 +792,7 @@ let attributes =
|
||||
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
|
||||
}
|
||||
test "_hxSync succeeds" {
|
||||
nav [ _hxSync "closest form" HxSync.Abort ] []
|
||||
|> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
|
||||
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
|
||||
}
|
||||
test "_hxTarget succeeds" {
|
||||
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
|
||||
@@ -842,31 +812,22 @@ let attributes =
|
||||
}
|
||||
]
|
||||
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
/// Tests for the Script module
|
||||
let script =
|
||||
testList "Script" [
|
||||
test "local succeeds" {
|
||||
let html = RenderView.AsString.htmlNode Script.local
|
||||
test "minified succeeds" {
|
||||
let html = RenderView.AsString.htmlNode Script.minified
|
||||
Expect.equal
|
||||
html
|
||||
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
|
||||
"Local script tag is incorrect"
|
||||
"""<script src="https://unpkg.com/htmx.org@2.0.4" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" crossorigin="anonymous"></script>"""
|
||||
"Minified script tag is incorrect"
|
||||
}
|
||||
test "cdnMinified succeeds" {
|
||||
let html = RenderView.AsString.htmlNode Script.cdnMinified
|
||||
test "unminified succeeds" {
|
||||
let html = RenderView.AsString.htmlNode Script.unminified
|
||||
Expect.equal
|
||||
html
|
||||
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script>"""
|
||||
"CDN minified 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="sha384-Q+Dky3iHVJOr6wUjQ4ulh6uQ76an/t+ak1+PjMVaxRjbZamFLAG+u9InkfjbsEQf" crossorigin="anonymous"></script>"""
|
||||
"CDN unminified script tag is incorrect"
|
||||
"""<script src="https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" integrity="sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" crossorigin="anonymous"></script>"""
|
||||
"Unminified script tag is incorrect"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -875,11 +836,11 @@ open System.Text
|
||||
/// Tests for the RenderFragment module
|
||||
let renderFragment =
|
||||
testList "RenderFragment" [
|
||||
|
||||
|
||||
/// Validate that the two object references are the same object
|
||||
let isSame obj1 obj2 message =
|
||||
Expect.isTrue (obj.ReferenceEquals(obj1, obj2)) message
|
||||
|
||||
Expect.isTrue (obj.ReferenceEquals (obj1, obj2)) message
|
||||
|
||||
testList "findIdNode" [
|
||||
test "fails with a Text node" {
|
||||
Expect.isNone (RenderFragment.findIdNode "blue" (Text "")) "There should not have been a node found"
|
||||
@@ -945,7 +906,7 @@ let renderFragment =
|
||||
]
|
||||
]
|
||||
testList "AsBytes" [
|
||||
|
||||
|
||||
/// Alias for UTF-8 encoding
|
||||
let utf8 = Encoding.UTF8
|
||||
|
||||
@@ -960,7 +921,7 @@ let renderFragment =
|
||||
}
|
||||
test "fails when an ID is not matched" {
|
||||
Expect.equal
|
||||
(RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes(nodeNotFound "whiff"))
|
||||
(RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes (nodeNotFound "whiff"))
|
||||
"HTML bytes are incorrect"
|
||||
}
|
||||
]
|
||||
@@ -977,7 +938,7 @@ let renderFragment =
|
||||
}
|
||||
test "fails when an ID is not matched" {
|
||||
Expect.equal
|
||||
(RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes(nodeNotFound "foo"))
|
||||
(RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes (nodeNotFound "foo"))
|
||||
"HTML bytes are incorrect"
|
||||
}
|
||||
]
|
||||
@@ -985,31 +946,31 @@ let renderFragment =
|
||||
testList "IntoStringBuilder" [
|
||||
testList "htmlFromNodes" [
|
||||
test "succeeds when an ID is matched" {
|
||||
let sb = StringBuilder()
|
||||
let sb = StringBuilder ()
|
||||
RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me"
|
||||
[ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]]
|
||||
Expect.equal (string sb) """<span id="find-me">;)</span>""" "HTML is incorrect"
|
||||
}
|
||||
test "fails when an ID is not matched" {
|
||||
let sb = StringBuilder()
|
||||
let sb = StringBuilder ()
|
||||
RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" []
|
||||
Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect"
|
||||
}
|
||||
]
|
||||
testList "htmlFromNode" [
|
||||
test "succeeds when ID is matched at top level" {
|
||||
let sb = StringBuilder()
|
||||
let sb = StringBuilder ()
|
||||
RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ])
|
||||
Expect.equal (string sb) """<p id="top">pinnacle</p>""" "HTML is incorrect"
|
||||
}
|
||||
test "succeeds when ID is matched in child element" {
|
||||
let sb = StringBuilder()
|
||||
let sb = StringBuilder ()
|
||||
div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]]
|
||||
|> RenderFragment.IntoStringBuilder.htmlFromNode sb "it"
|
||||
Expect.equal (string sb) """<p id="it">is here</p>""" "HTML is incorrect"
|
||||
}
|
||||
test "fails when an ID is not matched" {
|
||||
let sb = StringBuilder()
|
||||
let sb = StringBuilder ()
|
||||
RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr [])
|
||||
Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect"
|
||||
}
|
||||
@@ -1025,7 +986,6 @@ let allTests =
|
||||
hxHeaders
|
||||
hxParams
|
||||
hxRequest
|
||||
hxSync
|
||||
hxTrigger
|
||||
hxVals
|
||||
attributes
|
||||
|
||||
+285
-587
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
|
||||
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
|
||||
|
||||
**htmx version: 2.0.10**
|
||||
**htmx version: 2.0.4**
|
||||
|
||||
_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_
|
||||
|
||||
@@ -29,7 +29,7 @@ Support modules include:
|
||||
- `HxTrigger`
|
||||
- `HxVals`
|
||||
|
||||
`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).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user