Compare commits

...

3 Commits

Author SHA1 Message Date
danieljsummers 59b64ed834 v4, beta5 (#20)
Reviewed-on: #20
2026-06-30 02:03:15 +00:00
danieljsummers 5c12ad7cef v4, beta4 (#19)
- Update with header/attribute changes between alpha8 and beta4
- Add support for htmax script, attributes, headers, and extension config

Reviewed-on: #19
2026-06-15 01:07:38 +00:00
danieljsummers d4a7e0c9ce htmx v4, alpha8 (#18)
Reviewed-on: #18
2026-03-22 22:21:22 +00:00
23 changed files with 1405 additions and 526 deletions
+5 -5
View File
@@ -10,11 +10,11 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
`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. `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.
| Server Side | View Engine | | Server Side | View Engine |
|---|---| |------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|[![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/vpre/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/) | [![Nuget](https://img.shields.io/nuget/vpre/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)_. 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)_ and htmax htmx-plus-extensions bundle _(as of v4.0.0-beta4)_.
## Server Side (`Giraffe.Htmx`) ## Server Side (`Giraffe.Htmx`)
@@ -74,7 +74,7 @@ If you want to use the package-provided htmx library, `Htmx.Script.local` will c
## Feedback / Help ## Feedback / Help
The author hangs out in the #dotnet-htmx channel (and most others) of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/). The author hangs out in the #dotnet-htmx channel (and most others) of the [htmx Discord server](https://htmx.org/discord) and the #web channel (and most others) of the [F# Community Discord server](https://discord.gg/R6n7c54).
## Thanks ## Thanks
|[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)| [<img src="https://bitbadger.solutions/upload/bit-badger/2024/01/htmx-black-transparent.svg" alt="htmx logo" width="200">](https://htmx.org) |[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main)" width="200">](https://jb.gg/OpenSource)| |[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)| [<img src="https://bitbadger.solutions/upload/bit-badger/2024/01/htmx-black-transparent.svg" alt="htmx logo" width="200">](https://htmx.org) |[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main)" width="200">](https://jb.gg/OpenSource)|
+20 -4
View File
@@ -3,10 +3,15 @@
module Giraffe.Htmx.Common module Giraffe.Htmx.Common
/// <summary>The version of htmx embedded in the package</summary> /// <summary>The version of htmx embedded in the package</summary>
let HtmxVersion = "4.0.0-alpha6" let HtmxVersion = "4.0.0-beta5"
/// <summary>The path for the provided htmx script</summary> /// <summary>URLs for the included htmx library static web assets</summary>
let internal htmxLocalScript = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}" module StaticAssetUrl =
/// <summary>The path for the provided htmx script</summary>
let htmx = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
/// <summary>The path for the provided htmax script</summary>
let htmax = $"/_content/Giraffe.Htmx.Common/htmax.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>
@@ -44,7 +49,12 @@ module HxSwap =
/// <summary>Morph the outer HTML of the target to the new content</summary> /// <summary>Morph the outer HTML of the target to the new content</summary>
[<Literal>] [<Literal>]
let OuterMorph = "innerMorph" let OuterMorph = "outerMorph"
/// <summary>Morph the outer HTML of the target to the new content, recreating all children</summary>
/// <remarks>This is used internally by the new history extension, but can be used by others if desired</remarks>
[<Literal>]
let OuterSync = "outerSync"
/// <summary>Replace the text content of the target without parsing the response as HTML</summary> /// <summary>Replace the text content of the target without parsing the response as HTML</summary>
[<Literal>] [<Literal>]
@@ -95,3 +105,9 @@ module HxSwap =
/// <seealso href="https://four.htmx.org/extensions/upsert">Extension</seealso> /// <seealso href="https://four.htmx.org/extensions/upsert">Extension</seealso>
[<Literal>] [<Literal>]
let Upsert = "upsert" let Upsert = "upsert"
/// <summary>Specify that the target of the htmx request should be downloaded</summary>
/// <remarks>This requires the <c>hx-download</c> extension (included in the htmax bundle)</remarks>
/// <seealso href="https://four.htmx.org/extensions/hx-download#explicit-swap-style">Documentation</seealso>
[<Literal>]
let Download = "download"
+1 -1
View File
@@ -14,7 +14,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Update="FSharp.Core" Version="6.0.0" /> <PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+2 -2
View File
@@ -1,7 +1,7 @@
## 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. It also contains htmx and htmax as static web assets, allowing them to be loaded from your local (or published) project.
**htmx version: 4.0.0-alpha6** **htmx version: 4.0.0-beta5**
_**NOTE:** Pay special attention to breaking changes highlighted in the packages listed above._ _**NOTE:** Pay special attention to breaking changes highlighted in the packages listed above._
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+8 -7
View File
@@ -3,15 +3,16 @@
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<VersionPrefix>4.0.0</VersionPrefix> <VersionPrefix>4.0.0</VersionPrefix>
<VersionSuffix>alpha6a</VersionSuffix> <VersionSuffix>beta5</VersionSuffix>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>First htmx 4 alpha release of these libraries <PackageReleaseNotes>Update htmx 4 to beta5
- [Server] Marked removed headers as obsolete; added new HX-Source header - [Common] Update provided htmx/htmax 4 to 4.0.0-beta5
- [View Engine] Marked removed attributes as obsolete - [Common] Add StaticAssetUrl module with static asset paths for htmx and htmax
- [View Engine] Added new attributes, modifiers, and support for new hx-partial tag - [Server] Unobsolete HX-Prompt header, note that it requires hx-prompt extension
- Updated script tags to pull htmx 4.0.0-alpha6 - [View Engine] Unobsolete hx-prompt attribute, note that it requires hx-prompt extension
- [View Engine] Updated CDN script tags to pull htmx / htmax 4.0.0-beta5
See package READMEs; this is not an update-and-forget-it release See package and prior alpha release READMEs; v2 to v4 is not an update-and-forget-it release
</PackageReleaseNotes> </PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Expecto" Version="11.0.0" />
<PackageVersion Include="Giraffe" Version="8.2.0" />
<PackageVersion Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="FSharp.Core" Version="10.1.301" />
<!-- <PackageVersion Update="FSharp.Core" Version="10.0.0" /> -->
</ItemGroup>
</Project>
-40
View File
@@ -1,40 +0,0 @@
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
+6
View File
@@ -0,0 +1,6 @@
<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>
+2 -2
View File
@@ -14,8 +14,8 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="6.4.0" /> <PackageReference Include="Giraffe" />
<PackageReference Update="FSharp.Core" Version="6.0.0" /> <PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+64 -7
View File
@@ -4,6 +4,14 @@ open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Primitives open Microsoft.Extensions.Primitives
open System open System
/// <summary>The request types which may be set in the <c>HX-Request</c> header</summary>
type HxRequestTypes =
/// <summary>A request targeting the <c>body</c> tag or using an <c>hx-select</c> attribute</summary>
| HxFullRequest
/// <summary>A request for partial content</summary>
| HxPartialRequest
/// Determine if the given header is present /// Determine if the given header is present
let private hdr (headers : IHeaderDictionary) hdr = let private hdr (headers : IHeaderDictionary) hdr =
match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0] match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0]
@@ -23,8 +31,13 @@ type IHeaderDictionary with
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><c>true</c> if the request has been fired by the preload extension</summary>
/// <remarks><c>preload</c> is part of the htmax htmx-plus-extensions bundle</remarks>
member this.HxPreloaded
with get () = hdr this "HX-Preloaded" |> Option.map bool.Parse
/// <summary>The user response to an <c>hx-prompt</c></summary> /// <summary>The user response to an <c>hx-prompt</c></summary>
[<Obsolete "hx-prompt is removed in v4">] /// <remarks><b>NEW IN v4:</b> This functionality is dependent on the hx-prompt extension being loaded</remarks>
member this.HxPrompt member this.HxPrompt
with get () = hdr this "HX-Prompt" with get () = hdr this "HX-Prompt"
@@ -32,6 +45,21 @@ type IHeaderDictionary with
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 ID of the request (WebSocket extension requests only)</summary>
/// <remarks><c>hx-ws</c> is part of the htmax htmx-plus-extensions bundle</remarks>
member this.HxRequestId
with get () = hdr this "HX-Request-ID"
/// <summary>The request type sent by htmx</summary>
/// <seealso cref="HxRequestTypes" />
member this.HxRequestType
with get () =
match hdr this "HX-Request-Type" with
| Some typ when typ = "full" -> Some HxFullRequest
| Some typ when typ = "partial" -> Some HxPartialRequest
| Some _ -> None
| None -> None
/// <summary>The tag name (fst) and <c>id</c> attribute (snd) of the element triggering this request</summary> /// <summary>The tag name (fst) and <c>id</c> attribute (snd) of the element triggering this request</summary>
member this.HxSource member this.HxSource
with get () = with get () =
@@ -45,8 +73,23 @@ type IHeaderDictionary with
| None -> None | None -> None
/// <summary>The <c>id</c> attribute of the target element if it exists</summary> /// <summary>The <c>id</c> attribute of the target element if it exists</summary>
/// <remarks>
/// In v4, this changed to tag name (fst) and <c>id</c> (snd); to resolve build errors, and restore the prior
/// behavior of <c>[id] option</c>, replace
/// <code>ctx.HxTarget</code>
/// with
/// <code>ctx.HxTarget |> Option.iter snd</code>
/// </remarks>
member this.HxTarget member this.HxTarget
with get () = hdr this "HX-Target" with get () =
match hdr this "HX-Target" with
| Some src ->
let parts = src.Split "#"
if parts.Length = 1 then
Some (parts[0], None)
else
Some (parts[0], if parts[1] <> "" then Some parts[1] else None)
| None -> None
/// <summary>The <c>id</c> attribute of the triggered element if it exists</summary> /// <summary>The <c>id</c> attribute of the triggered element if it exists</summary>
[<Obsolete "HX-Trigger is removed in v4; use the second item of HX-Source">] [<Obsolete "HX-Trigger is removed in v4; use the second item of HX-Source">]
@@ -77,6 +120,14 @@ module Handlers =
open Giraffe.Htmx.Common open Giraffe.Htmx.Common
/// <summary>Instruct htmx to download a response from another path / URL</summary>
/// <param name="path">The path or URL where the downloadable content is found</param>
/// <returns>An HTTP handler with the <c>HX-Download</c> header set</returns>
/// <remarks>This requires the client-side <c>hx-download</c> extension (included in the htmax bundle)</remarks>
/// <seealso href="https://four.htmx.org/extensions/hx-download#hx-download-header">Documentation</seealso>
let withHxDownload (path: string) : HttpHandler =
setHttpHeader "HX-Download" path
/// <summary>Instruct htmx to perform a client-side redirect for content</summary> /// <summary>Instruct htmx to perform a client-side redirect for content</summary>
/// <param name="path">The path where the content should be found</param> /// <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> /// <returns>An HTTP handler with the <c>HX-Location</c> header set</returns>
@@ -162,29 +213,33 @@ module Handlers =
/// <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 <c>HX-Trigger-After-Settle</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso> /// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerAfterSettle (evt: string) : HttpHandler = let withHxTriggerAfterSettle (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger-After-Settle" evt setHttpHeader "HX-Trigger" 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 <c>HX-Trigger-After-Settle</c> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso> /// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerManyAfterSettle evts : HttpHandler = let withHxTriggerManyAfterSettle evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle" toJson evts |> setHttpHeader "HX-Trigger"
/// <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 <c>HX-Trigger-After-Swap</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso> /// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerAfterSwap (evt: string) : HttpHandler = let withHxTriggerAfterSwap (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger-After-Swap" evt setHttpHeader "HX-Trigger" 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 <c>HX-Trigger-After-Swap</c> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso> /// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerManyAfterSwap evts : HttpHandler = let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap" toJson evts |> setHttpHeader "HX-Trigger"
/// <summary>Load the package-provided version of the htmx script</summary> /// <summary>Load the package-provided version of the htmx script</summary>
@@ -195,5 +250,7 @@ module HtmxScript =
open Microsoft.AspNetCore.Html open Microsoft.AspNetCore.Html
/// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary> /// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary>
let local = HtmlString $"""<script src="{htmxLocalScript}"></script>""" let local = HtmlString $"""<script src="{StaticAssetUrl.htmx}"></script>"""
/// <summary><c>script</c> tag to load the package-provided version of the htmax script</summary>
let localMax = HtmlString $"""<script src="{StaticAssetUrl.htmax}"></script>"""
+3 -3
View File
@@ -2,9 +2,9 @@
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: 4.0.0-alpha6** **htmx version: 4.0.0-beta5**
_Upgrading from v2.x: the [migration guide](https://four.htmx.org/migration-guide-htmx-4/) lists changes for v4. For this package, the `HX-Trigger` and `HX-Trigger-Name` headers are marked obsolete. They are replaced by `HX-Source`, which provides the triggering tag name and `id` attribute. The `HX-Prompt` header has also been marked as obsolete, as the `hx-prompt` attribute which generated its content has been removed._ _Upgrading from v2.x: the [migration guide](https://four.htmx.org/docs/get-started/migration) lists changes for v4. For this package, the `HX-Trigger` and `HX-Trigger-Name` headers are marked obsolete. They are replaced by `HX-Source`, which provides the triggering tag name and `id` attribute. The `HX-Prompt` header has also been marked as obsolete, as the `hx-prompt` attribute which generated its content has been removed._
_Obsolete elements will be removed in the first production v4 release._ _Obsolete elements will be removed in the first production v4 release._
@@ -36,7 +36,7 @@ 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`. To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`; to load the htmax bundle, use `HtmxScript.localMax`.
### Learn ### Learn
+25 -3
View File
@@ -6,9 +6,25 @@ open Giraffe.Htmx
/// Test to ensure the version was updated /// Test to ensure the version was updated
let version = let version =
test "HtmxVersion is correct" { test "HtmxVersion is correct" {
Expect.equal HtmxVersion "4.0.0-alpha6" "htmx version incorrect" Expect.equal HtmxVersion "4.0.0-beta5" "htmx version incorrect"
} }
let staticAssetUrl =
testList "StaticAssetUrl" [
test "htmx is correct" {
Expect.equal
StaticAssetUrl.htmx
$"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
"Static htmx URL incorrect"
}
test "htmax is correct" {
Expect.equal
StaticAssetUrl.htmax
$"/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"
"Static htmx URL incorrect"
}
]
/// Tests for the HxSwap module /// Tests for the HxSwap module
let swap = let swap =
testList "HxSwap" [ testList "HxSwap" [
@@ -22,7 +38,10 @@ let swap =
Expect.equal HxSwap.InnerMorph "innerMorph" "Inner Morph swap value incorrect" Expect.equal HxSwap.InnerMorph "innerMorph" "Inner Morph swap value incorrect"
} }
test "OuterMorph is correct" { test "OuterMorph is correct" {
Expect.equal HxSwap.OuterMorph "innerMorph" "Outer Morph swap value incorrect" Expect.equal HxSwap.OuterMorph "outerMorph" "Outer Morph swap value incorrect"
}
test "OuterSync is correct" {
Expect.equal HxSwap.OuterSync "outerSync" "Outer Sync swap value incorrect"
} }
test "TextContent is correct" { test "TextContent is correct" {
Expect.equal HxSwap.TextContent "textContent" "Text Content swap value incorrect" Expect.equal HxSwap.TextContent "textContent" "Text Content swap value incorrect"
@@ -60,7 +79,10 @@ let swap =
test "Upsert is correct" { test "Upsert is correct" {
Expect.equal HxSwap.Upsert "upsert" "Upsert swap value incorrect" Expect.equal HxSwap.Upsert "upsert" "Upsert swap value incorrect"
} }
test "Download is correct" {
Expect.equal HxSwap.Download "download" "Download swap value incorrect"
}
] ]
/// All tests for this module /// All tests for this module
let allTests = testList "Htmx.Common" [ version; swap ] let allTests = testList "Htmx.Common" [ version; staticAssetUrl; swap ]
+162 -58
View File
@@ -73,6 +73,45 @@ let dictExtensions =
ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header should have been false" ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header should have been false"
} }
] ]
testList "HxPreloaded" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxPreloaded "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-Preloaded", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxPreloaded "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxPreloaded.Value "The header should have been true"
}
// This is not a condition fired by the extension; the header will be either absent or true
test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Preloaded", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxPreloaded "There should be a header present"
Expect.isFalse ctx.Request.Headers.HxPreloaded.Value "The header should have been false"
}
]
testList "HxPrompt" [
test "succeeds when the header is not present" {
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")
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"
}
]
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>()
@@ -96,6 +135,51 @@ let dictExtensions =
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"
} }
] ]
testList "HxRequestId" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxRequestId "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-Request-ID", "abcd1234")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequestId "There should be a header present"
Expect.equal ctx.Request.Headers.HxRequestId.Value "abcd1234" "The header value was incorrect"
}
]
testList "HxRequestType" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxRequestType "There should not have been a header returned"
}
test "succeeds when the header is invalid" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Request-Type", "relaxed")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isNone ctx.Request.Headers.HxRequestType "There should not have been a header returned"
}
test "succeeds for a full request" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Request-Type", "full")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequestType "There have been a header returned"
Expect.equal ctx.Request.Headers.HxRequestType.Value HxFullRequest "The header value is incorrect"
}
test "succeeds for a partial request" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Request-Type", "partial")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequestType "There have been a header returned"
Expect.equal ctx.Request.Headers.HxRequestType.Value HxPartialRequest "The header value is incorrect"
}
]
testList "HxSource" [ testList "HxSource" [
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>()
@@ -140,13 +224,36 @@ let dictExtensions =
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 and both parts exist" {
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", "div#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" let hdr = ctx.Request.Headers.HxTarget
Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect" Expect.isSome hdr "There should be a header present"
Expect.equal (fst hdr.Value) "div" "The target tag was incorrect"
Expect.isSome (snd hdr.Value) "There should be a target ID present"
Expect.equal (snd hdr.Value).Value "leItem" "The header value was incorrect"
}
test "succeeds when the header is present and ID is blank" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Target", "span#")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
let hdr = ctx.Request.Headers.HxTarget
Expect.isSome hdr "There should be a header present"
Expect.equal (fst hdr.Value) "span" "The target tag was incorrect"
Expect.isNone (snd hdr.Value) "There should not be a target ID present"
}
test "succeeds when the header is present and ID is missing" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Target", "aside")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
let hdr = ctx.Request.Headers.HxTarget
Expect.isSome hdr "There should be a header present"
Expect.equal (fst hdr.Value) "aside" "The target tag was incorrect"
Expect.isNone (snd hdr.Value) "There should not be a target ID present"
} }
] ]
] ]
@@ -200,6 +307,14 @@ 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 "withHxDownload succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxDownload "/files/stuff.pdf" next ctx
Expect.isTrue (dic.ContainsKey "HX-Download") "The HX-Download header should be present"
Expect.equal dic["HX-Download"].[0] "/files/stuff.pdf" "The HX-Download value was incorrect"
}
testTask "withHxLocation succeeds" { testTask "withHxLocation succeeds" {
let ctx = Substitute.For<HttpContext>() let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary() let dic = HeaderDictionary()
@@ -307,44 +422,6 @@ let handlers =
Expect.equal Expect.equal
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" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Expect.isTrue
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present"
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()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Expect.isTrue
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present"
Expect.equal
dic["HX-Trigger-After-Settle"].[0] """{ "oof": "ouch", "hmm": "uh" }"""
"The HX-Trigger-After-Settle value was incorrect"
}
testTask "withHxTriggerAfterSwap succeeds" {
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()
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"
Expect.equal
dic["HX-Trigger-After-Swap"].[0] """{ "this": "1", "that": "2" }"""
"The HX-Trigger-After-Swap value was incorrect"
}
] ]
/// Tests for the HtmxScript module /// Tests for the HtmxScript module
@@ -356,26 +433,17 @@ let script =
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>""" $"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
"htmx script link is incorrect" "htmx script link is incorrect"
} }
test "localMax generates correct link" {
Expect.equal
(string HtmxScript.localMax)
$"""<script src="/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"></script>"""
"htmx script link is incorrect"
}
] ]
#nowarn 44 // Obsolete items still have tests #nowarn 44 // Obsolete items still have tests
let dictExtensionsObs = let dictExtensionsObs =
testList "IHeaderDictionaryExtensions (Obsolete)" [ testList "IHeaderDictionaryExtensions (Obsolete)" [
testList "HxPrompt" [
test "succeeds when the header is not present" {
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")
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"
}
]
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>()
@@ -408,5 +476,41 @@ let dictExtensionsObs =
] ]
] ]
let handlerObs =
testList "Handler Tests (Obsolete)" [
testTask "withHxTriggerAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] "byTheWay" "The HX-Trigger value was incorrect"
}
testTask "withHxTriggerManyAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] """{ "oof": "ouch", "hmm": "uh" }""" "The HX-Trigger value was incorrect"
}
testTask "withHxTriggerAfterSwap succeeds" {
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") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] "justASec" "The HX-Trigger value was incorrect"
}
testTask "withHxTriggerManyAfterSwap succeeds" {
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") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] """{ "this": "1", "that": "2" }""" "The HX-Trigger value was incorrect"
}
]
/// All tests for this module /// All tests for this module
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script; dictExtensionsObs ] let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script; dictExtensionsObs; handlerObs ]
+1 -1
View File
@@ -1,6 +1,6 @@
open Expecto open Expecto
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests ] let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests; ViewEngineMax.allTests ]
[<EntryPoint>] [<EntryPoint>]
let main args = runTestsWithCLIArgs [] args allTests let main args = runTestsWithCLIArgs [] args allTests
+4 -3
View File
@@ -8,6 +8,7 @@
<Compile Include="Common.fs" /> <Compile Include="Common.fs" />
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<Compile Include="ViewEngine.fs" /> <Compile Include="ViewEngine.fs" />
<Compile Include="ViewEngineMax.fs" />
<Compile Include="Program.fs" /> <Compile Include="Program.fs" />
</ItemGroup> </ItemGroup>
@@ -18,9 +19,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="10.2.1" /> <PackageReference Include="Expecto" />
<PackageReference Include="NSubstitute" Version="5.1.0" /> <PackageReference Include="NSubstitute" />
<PackageReference Update="FSharp.Core" Version="8.0.300" /> <PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+124 -71
View File
@@ -1,6 +1,7 @@
module ViewEngine module ViewEngine
open Expecto open Expecto
open Giraffe.Htmx
open Giraffe.ViewEngine open Giraffe.ViewEngine
open Giraffe.ViewEngine.Htmx open Giraffe.ViewEngine.Htmx
@@ -129,22 +130,12 @@ let hxEvent =
Expect.equal (AfterRequest.ToHxOnString()) "after:request" "AfterRequest hx-on event name not correct" Expect.equal (AfterRequest.ToHxOnString()) "after:request" "AfterRequest hx-on event name not correct"
} }
] ]
testList "AfterSseMessage" [ testList "AfterSettle" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct" Expect.equal (string AfterSettle) "afterSettle" "AfterSettle event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal Expect.equal (AfterSettle.ToHxOnString()) "after:settle" "AfterSettle hx-on event name not correct"
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
}
]
testList "AfterSseStream" [
test "ToString succeeds" {
Expect.equal (string AfterSseStream) "afterSseStream" "AfterSseStream event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseStream.ToHxOnString()) "after:sse:stream" "AfterSseStream hx-on event name not correct"
} }
] ]
testList "AfterSwap" [ testList "AfterSwap" [
@@ -184,6 +175,15 @@ let hxEvent =
Expect.equal (BeforeInit.ToHxOnString()) "before:init" "BeforeInit hx-on event name not correct" Expect.equal (BeforeInit.ToHxOnString()) "before:init" "BeforeInit hx-on event name not correct"
} }
] ]
testList "BeforeProcess" [
test "ToString succeeds" {
Expect.equal (string BeforeProcess) "beforeProcess" "BeforeProcess event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeProcess.ToHxOnString()) "before:process" "BeforeProcess hx-on event name not correct"
}
]
testList "BeforeRestoreHistory" [ testList "BeforeRestoreHistory" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal Expect.equal
@@ -205,36 +205,13 @@ let hxEvent =
(BeforeRequest.ToHxOnString()) "before:request" "BeforeRequest hx-on event name not correct" (BeforeRequest.ToHxOnString()) "before:request" "BeforeRequest hx-on event name not correct"
} }
] ]
testList "BeforeSseMessage" [ testList "BeforeResponse" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct" Expect.equal (string BeforeResponse) "beforeResponse" "BeforeResponse event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal Expect.equal
(BeforeSseMessage.ToHxOnString()) (BeforeResponse.ToHxOnString()) "before:response" "BeforeResponse hx-on event name not correct"
"before:sse:message"
"BeforeSseMessage hx-on event name not correct"
}
]
testList "BeforeSseReconnect" [
test "ToString succeeds" {
Expect.equal
(string BeforeSseReconnect) "beforeSseReconnect" "BeforeSseReconnect event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseReconnect.ToHxOnString())
"before:sse:reconnect"
"BeforeSseReconnect hx-on event name not correct"
}
]
testList "BeforeSseStream" [
test "ToString succeeds" {
Expect.equal (string BeforeSseStream) "beforeSseStream" "BeforeSseStream event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseStream.ToHxOnString()) "before:sse:stream" "BeforeSseStream hx-on event name not correct"
} }
] ]
testList "BeforeSwap" [ testList "BeforeSwap" [
@@ -279,6 +256,15 @@ let hxEvent =
(FinallyRequest.ToHxOnString()) "finally:request" "FinallyRequest hx-on event name not correct" (FinallyRequest.ToHxOnString()) "finally:request" "FinallyRequest hx-on event name not correct"
} }
] ]
testList "ResponseError" [
test "ToString succeeds" {
Expect.equal (string ResponseError) "responseError" "ResponseError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(ResponseError.ToHxOnString()) "response:error" "ResponseError hx-on event name not correct"
}
]
] ]
/// Tests for the HxHeaders module /// Tests for the HxHeaders module
@@ -572,7 +558,7 @@ let attributes =
} }
test "_hxOnHxEvent succeeds" { test "_hxOnHxEvent succeeds" {
strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] [] strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] []
|> shouldRender """<strong hx-on::before:swap="changeStuff()"></strong>""" |> shouldRender """<strong hx-on:htmx:before:swap="changeStuff()"></strong>"""
} }
test "_hxPatch succeeds" { test "_hxPatch succeeds" {
div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>""" div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>"""
@@ -583,6 +569,10 @@ let attributes =
test "_hxPreserve succeeds" { test "_hxPreserve succeeds" {
img [ _hxPreserve ] |> shouldRender """<img hx-preserve>""" img [ _hxPreserve ] |> shouldRender """<img hx-preserve>"""
} }
test "_hxPrompt succeeds" {
strong [ _hxPrompt "Who goes there?" ] []
|> shouldRender """<strong hx-prompt="Who goes there?"></strong>"""
}
test "_hxPushUrl succeeds" { test "_hxPushUrl succeeds" {
dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """<dl hx-push-url="/a-b-c"></dl>""" dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """<dl hx-push-url="/a-b-c"></dl>"""
} }
@@ -598,6 +588,18 @@ let attributes =
test "_hxSelectOob succeeds" { test "_hxSelectOob succeeds" {
section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>""" section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
} }
testList "_hxStatus" [
test "succeeds for a full code with no action" {
div [ _hxStatus "403" HxSwap.After "" ] [] |> shouldRender """<div hx-status:403="swap:after"></div>"""
}
test "succeeds for a two-digit range with action" {
span [ _hxStatus "41" HxSwap.Before "target:main" ] []
|> shouldRender """<span hx-status:41x="swap:before target:main"></span>"""
}
test "succeeds for a one-digit range with no action" {
p [ _hxStatus "5" HxSwap.None "" ] [] |> shouldRender """<p hx-status:5xx="swap:none"></p>"""
}
]
test "_hxSwap succeeds" { test "_hxSwap succeeds" {
del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>""" del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>"""
} }
@@ -625,12 +627,6 @@ let attributes =
dt [ _hxVals """{ "extra": "values" }""" ] [] dt [ _hxVals """{ "extra": "values" }""" ] []
|> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>""" |> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>"""
} }
test "_sseConnect succeeds" {
div [ _sseConnect "/gps/sse" ] [] |> shouldRender """<div sse-connect="/gps/sse"></div>"""
}
test "_sseSwap succeeds" {
ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """<ul sse-swap="sseMessageName"></ul>"""
}
] ]
/// Tests for the HxModifiers module /// Tests for the HxModifiers module
@@ -671,8 +667,6 @@ let hxTags =
} }
] ]
open Giraffe.Htmx.Common
/// Tests for the Script module /// Tests for the Script module
let script = let script =
testList "Script" [ testList "Script" [
@@ -687,16 +681,39 @@ let script =
let html = RenderView.AsString.htmlNode Script.cdnMinified let html = RenderView.AsString.htmlNode Script.cdnMinified
Expect.equal Expect.equal
html html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-/5n21KGM472oBhvzUrvju8FRDq/4WNMS3TGw5RWFkZR/kq+sCevqNXFcakqRtaHu" crossorigin="anonymous"></script>""" $"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-5dnhUXCt1hXGvYrjAnKwgNX3I8xtIJiW6eIHIbeo7oWyXv2XpWYC/rl+ZiWfuYO5" crossorigin="anonymous"></script>"""
"CDN minified script tag is incorrect" "CDN minified script tag is incorrect"
} }
test "cdnUnminified succeeds" { test "cdnUnminified succeeds" {
let html = RenderView.AsString.htmlNode Script.cdnUnminified let html = RenderView.AsString.htmlNode Script.cdnUnminified
Expect.equal Expect.equal
html html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-I3uRy2RjSNcS8Oi2SNWZD9IrafyrrrBb4QaHNA9pWVWfJuKtL0p8qAwHSrEyXoKh" crossorigin="anonymous"></script>""" $"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-RZoQSZlu2BAuZMuM5lTKAWXXSKC+7X6eVzP1pwkUBcyfPmOswexqVOsUqQMKbAFA" crossorigin="anonymous"></script>"""
"CDN unminified script tag is incorrect" "CDN unminified script tag is incorrect"
} }
testList "Max" [
test "localMax succeeds" {
let html = RenderView.AsString.htmlNode Script.Max.local
Expect.equal
html
$"""<script src="/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"></script>"""
"Local script tag is incorrect"
}
test "cdnMaxMinified succeeds" {
let html = RenderView.AsString.htmlNode Script.Max.cdnMinified
Expect.equal
html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.min.js" integrity="sha384-VVbrNR6a+H8puV17ZlJ8aUUMTgbcDiqM1vlLYEmaaU4oFANllvTK2pLynNonElP+" crossorigin="anonymous"></script>"""
"CDN minified script tag is incorrect"
}
test "cdnMaxUnminified succeeds" {
let html = RenderView.AsString.htmlNode Script.Max.cdnUnminified
Expect.equal
html
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.js" integrity="sha384-kjhVuvnX3/TsR1qH4JaIcHR6muh/WLMU5CTQRacCQZERzQlP4/r9p/TK7ucFwqvV" crossorigin="anonymous"></script>"""
"CDN unminified script tag is incorrect"
}
]
] ]
open System.Text open System.Text
@@ -864,15 +881,25 @@ let hxEventObs =
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal Expect.equal
(AfterProcessNode.ToHxOnString()) "after:init" "AfterProcessNode hx-on event name not correct" (AfterProcessNode.ToHxOnString()) "after:process" "AfterProcessNode hx-on event name not correct"
} }
] ]
testList "AfterSettle" [ testList "AfterSseMessage" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal (string AfterSettle) "afterSettle" "AfterSettle event name not correct" Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal (AfterSettle.ToHxOnString()) "after:swap" "AfterSettle hx-on event name not correct" Expect.equal
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
}
]
testList "AfterSseStream" [
test "ToString succeeds" {
Expect.equal (string AfterSseStream) "afterSseStream" "AfterSseStream event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseStream.ToHxOnString()) "after:sse:stream" "AfterSseStream hx-on event name not correct"
} }
] ]
testList "BeforeCleanupElement" [ testList "BeforeCleanupElement" [
@@ -912,7 +939,7 @@ let hxEventObs =
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal Expect.equal
(BeforeProcessNode.ToHxOnString()) "before:init" "BeforeProcessNode hx-on event name not correct" (BeforeProcessNode.ToHxOnString()) "before:process" "BeforeProcessNode hx-on event name not correct"
} }
] ]
testList "BeforeSend" [ testList "BeforeSend" [
@@ -923,6 +950,38 @@ let hxEventObs =
Expect.equal (BeforeSend.ToHxOnString()) "before:request" "BeforeSend hx-on event name not correct" Expect.equal (BeforeSend.ToHxOnString()) "before:request" "BeforeSend hx-on event name not correct"
} }
] ]
testList "BeforeSseMessage" [
test "ToString succeeds" {
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseMessage.ToHxOnString())
"before:sse:message"
"BeforeSseMessage hx-on event name not correct"
}
]
testList "BeforeSseReconnect" [
test "ToString succeeds" {
Expect.equal
(string BeforeSseReconnect) "beforeSseReconnect" "BeforeSseReconnect event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseReconnect.ToHxOnString())
"before:sse:reconnect"
"BeforeSseReconnect hx-on event name not correct"
}
]
testList "BeforeSseStream" [
test "ToString succeeds" {
Expect.equal (string BeforeSseStream) "beforeSseStream" "BeforeSseStream event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseStream.ToHxOnString()) "before:sse:stream" "BeforeSseStream hx-on event name not correct"
}
]
testList "HistoryCacheError" [ testList "HistoryCacheError" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal (string HistoryCacheError) "historyCacheError" "HistoryCacheError event name not correct" Expect.equal (string HistoryCacheError) "historyCacheError" "HistoryCacheError event name not correct"
@@ -1049,14 +1108,6 @@ let hxEventObs =
"PushedIntoHistory hx-on event name not correct" "PushedIntoHistory hx-on event name not correct"
} }
] ]
testList "ResponseError" [
test "ToString succeeds" {
Expect.equal (string ResponseError) "responseError" "ResponseError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (ResponseError.ToHxOnString()) "error" "ResponseError hx-on event name not correct"
}
]
testList "SendError" [ testList "SendError" [
test "ToString succeeds" { test "ToString succeeds" {
Expect.equal (string SendError) "sendError" "SendError event name not correct" Expect.equal (string SendError) "sendError" "SendError event name not correct"
@@ -1070,7 +1121,7 @@ let hxEventObs =
Expect.equal (string SseError) "sseError" "SseError event name not correct" Expect.equal (string SseError) "sseError" "SseError event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal (SseError.ToHxOnString()) "error" "SseError hx-on event name not correct" Expect.equal (SseError.ToHxOnString()) "sse:error" "SseError hx-on event name not correct"
} }
] ]
testList "SseOpen" [ testList "SseOpen" [
@@ -1078,7 +1129,7 @@ let hxEventObs =
Expect.equal (string SseOpen) "sseOpen" "SseOpen event name not correct" Expect.equal (string SseOpen) "sseOpen" "SseOpen event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal (SseOpen.ToHxOnString()) "sse-open" "SseOpen hx-on event name not correct" Expect.equal (SseOpen.ToHxOnString()) "after:sse:connection" "SseOpen hx-on event name not correct"
} }
] ]
testList "SwapError" [ testList "SwapError" [
@@ -1144,7 +1195,7 @@ let hxEventObs =
Expect.equal (string XhrAbort) "xhr:abort" "XhrAbort event name not correct" Expect.equal (string XhrAbort) "xhr:abort" "XhrAbort event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal (XhrAbort.ToHxOnString()) "xhr:abort" "XhrAbort hx-on event name not correct" Expect.equal (XhrAbort.ToHxOnString()) "error" "XhrAbort hx-on event name not correct"
} }
] ]
testList "XhrLoadEnd" [ testList "XhrLoadEnd" [
@@ -1152,7 +1203,7 @@ let hxEventObs =
Expect.equal (string XhrLoadEnd) "xhr:loadend" "XhrLoadEnd event name not correct" Expect.equal (string XhrLoadEnd) "xhr:loadend" "XhrLoadEnd event name not correct"
} }
test "ToHxOnString succeeds" { test "ToHxOnString succeeds" {
Expect.equal (XhrLoadEnd.ToHxOnString()) "xhr:loadend" "XhrLoadEnd hx-on event name not correct" Expect.equal (XhrLoadEnd.ToHxOnString()) "finally:request" "XhrLoadEnd hx-on event name not correct"
} }
] ]
testList "XhrLoadStart" [ testList "XhrLoadStart" [
@@ -1267,13 +1318,15 @@ let attributesObs =
test "_hxParams succeeds" { test "_hxParams succeeds" {
br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">""" br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">"""
} }
test "_hxPrompt succeeds" {
strong [ _hxPrompt "Who goes there?" ] []
|> shouldRender """<strong hx-prompt="Who goes there?"></strong>"""
}
test "_hxRequest succeeds" { test "_hxRequest succeeds" {
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>""" u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
} }
test "_sseConnect succeeds" {
div [ _sseConnect "/gps/sse" ] [] |> shouldRender """<div sse-connect="/gps/sse"></div>"""
}
test "_sseSwap succeeds" {
ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """<ul sse-swap="sseMessageName"></ul>"""
}
] ]
let obsolete = testList "Obsolete" [ hxEventObs; hxParamsObs; hxRequestObs; attributesObs ] let obsolete = testList "Obsolete" [ hxEventObs; hxParamsObs; hxRequestObs; attributesObs ]
+335
View File
@@ -0,0 +1,335 @@
module ViewEngineMax
open Expecto
open Giraffe.ViewEngine.Htmax
/// Tests for the HxBrowserIndicatorConfigItem type
let hxBrowserIndicatorConfigItem =
testList "HxBrowserIndicatorConfigItem" [
testList "ToHcon" [
test "succeeds for BoostBrowserIndicator" {
Expect.equal
((BoostBrowserIndicator false).ToHcon())
"boostBrowserIndicator:false"
"The HCON value was incorrect"
}
]
]
/// Tests for the HxPreloadConfigItem type
let hxPreloadConfigItem =
testList "HxPreloadConfigItem" [
testList "ToHcon" [
test "succeeds for AutoBoost" {
Expect.equal ((AutoBoost false).ToHcon()) "autoBoost:false" "The HCON value was incorrect"
}
test "succeeds for BoostEvent" {
Expect.equal ((BoostEvent "mouseover").ToHcon()) "boostEvent:mouseover" "The HCON value was incorrect"
}
test "succeeds for BoostTimeout" {
Expect.equal ((BoostTimeout 30_000).ToHcon()) "boostTimeout:30000" "The HCON value was incorrect"
}
]
]
/// Tests for the HxSseWsConfigItem type
let hxSseWsConfigItem =
testList "HxSseWsConfigItem" [
testList "ToHcon" [
test "succeeds for Reconnect" {
Expect.equal ((Reconnect true).ToHcon()) "reconnect:true" "The HCON value was incorrect"
}
testList "ReconnectDelay" [
test "succeeds for numeric value" {
Expect.equal ((ReconnectDelay "3000").ToHcon()) "reconnectDelay:3000" "The HCON value was incorrect"
}
test "succeeds for value with units" {
Expect.equal ((ReconnectDelay "3s").ToHcon()) "reconnectDelay:3s" "The HCON value was incorrect"
}
]
testList "ReconnectMaxDelay" [
test "succeeds for numeric value" {
Expect.equal
((ReconnectMaxDelay "30000").ToHcon()) "reconnectMaxDelay:30000" "The HCON value was incorrect"
}
test "succeeds for value with units" {
Expect.equal
((ReconnectMaxDelay "50s").ToHcon()) "reconnectMaxDelay:50s" "The HCON value was incorrect"
}
]
testList "ReconnectMaxAttempts" [
test "succeeds for negative 1" {
Expect.equal
((ReconnectMaxAttempts -1).ToHcon())
"reconnectMaxAttempts:Infinity"
"The HCON value was incorrect"
}
test "succeeds for zero" {
Expect.equal
((ReconnectMaxAttempts 0).ToHcon()) "reconnectMaxAttempts:0" "The HCON value was incorrect"
}
test "succeeds for positive number" {
Expect.equal
((ReconnectMaxAttempts 7).ToHcon()) "reconnectMaxAttempts:7" "The HCON value was incorrect"
}
]
test "succeeds for ReconnectJitter" {
Expect.equal ((ReconnectJitter 0.7).ToHcon()) "reconnectJitter:0.7" "The HCON value was incorrect"
}
test "succeeds for PauseOnBackground" {
Expect.equal
((PauseOnBackground false).ToHcon()) "pauseOnBackground:false" "The HCON value was incorrect"
}
test "succeeds for PendingRequestTtl" {
Expect.equal ((PendingRequestTtl 5000).ToHcon()) "pendingRequestTTL:5000" "The HCON value was incorrect"
}
]
]
/// Tests for the HxConfig module
let hxConfig =
testList "HxConfig" [
test "hxBrowserIndicatorConfig succeeds" {
Expect.equal
(hxBrowserIndicatorConfig [ BoostBrowserIndicator true ])
"browser-indicator.boostBrowserIndicator:true"
"Config settings not correct"
}
test "hxPreloadConfig succeeds" {
Expect.equal
(hxPreloadConfig [ AutoBoost true; BoostTimeout 3_000 ])
"preload.autoBoost:true preload.boostTimeout:3000"
"Config settings not correct"
}
test "hxSseConfig succeeds" {
Expect.equal
(hxSseConfig [ ReconnectDelay "5s"; PauseOnBackground false ])
"sse.reconnectDelay:5s sse.pauseOnBackground:false"
"Config settings not correct"
}
test "hxWsConfig succeeds" {
Expect.equal
(hxWsConfig [ Reconnect false; PendingRequestTtl 40_000 ])
"ws.reconnect:false ws.pendingRequestTTL:40000"
"Config settings not correct"
}
]
/// Tests for the HxEvent module
let hxEvent =
testList "HxEvent" [
testList "AfterSseConnection" [
test "ToString succeeds" {
Expect.equal
(string AfterSseConnection) "afterSseConnection" "AfterSseConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseConnection.ToHxOnString())
"after:sse:connection"
"AfterSseConnection hx-on event name not correct"
}
]
testList "AfterSseMessage" [
test "ToString succeeds" {
Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
}
]
testList "AfterWsConnection" [
test "ToString succeeds" {
Expect.equal
(string AfterWsConnection) "afterWsConnection" "AfterWsConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsConnection.ToHxOnString())
"after:ws:connection"
"AfterWsConnection hx-on event name not correct"
}
]
testList "AfterWsMessage" [
test "ToString succeeds" {
Expect.equal (string AfterWsMessage) "afterWsMessage" "AfterWsMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsMessage.ToHxOnString()) "after:ws:message" "AfterWsMessage hx-on event name not correct"
}
]
testList "AfterWsRequest" [
test "ToString succeeds" {
Expect.equal (string AfterWsRequest) "afterWsRequest" "AfterWsRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsRequest.ToHxOnString()) "after:ws:request" "AfterWsRequest hx-on event name not correct"
}
]
testList "BeforeSseConnection" [
test "ToString succeeds" {
Expect.equal
(string BeforeSseConnection) "beforeSseConnection" "BeforeSseConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseConnection.ToHxOnString())
"before:sse:connection"
"BeforeSseConnection hx-on event name not correct"
}
]
testList "BeforeSseMessage" [
test "ToString succeeds" {
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseMessage.ToHxOnString())
"before:sse:message"
"BeforeSseMessage hx-on event name not correct"
}
]
testList "BeforeWsConnection" [
test "ToString succeeds" {
Expect.equal
(string BeforeWsConnection) "beforeWsConnection" "BeforeWsConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsConnection.ToHxOnString())
"before:ws:connection"
"BeforeWsConnection hx-on event name not correct"
}
]
testList "BeforeWsMessage" [
test "ToString succeeds" {
Expect.equal (string BeforeWsMessage) "beforeWsMessage" "BeforeWsMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsMessage.ToHxOnString()) "before:ws:message" "BeforeWsMessage hx-on event name not correct"
}
]
testList "BeforeWsRequest" [
test "ToString succeeds" {
Expect.equal (string BeforeWsRequest) "beforeWsRequest" "BeforeWsRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsRequest.ToHxOnString()) "before:ws:request" "BeforeWsRequest hx-on event name not correct"
}
]
testList "DownloadComplete" [
test "ToString succeeds" {
Expect.equal (string DownloadComplete) "downloadComplete" "DownloadComplete event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadComplete.ToHxOnString())
"download:complete"
"DownloadComplete hx-on event name not correct"
}
]
testList "DownloadProgress" [
test "ToString succeeds" {
Expect.equal (string DownloadProgress) "downloadProgress" "DownloadProgress event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadProgress.ToHxOnString())
"download:progress"
"DownloadProgress hx-on event name not correct"
}
]
testList "DownloadStart" [
test "ToString succeeds" {
Expect.equal (string DownloadStart) "downloadStart" "DownloadStart event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadStart.ToHxOnString()) "download:start" "DownloadStart hx-on event name not correct"
}
]
testList "SseClose" [
test "ToString succeeds" {
Expect.equal (string SseClose) "sseClose" "SseClose event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SseClose.ToHxOnString()) "sse:close" "SseClose hx-on event name not correct"
}
]
testList "SseError" [
test "ToString succeeds" {
Expect.equal (string SseError) "sseError" "SseError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SseError.ToHxOnString()) "sse:error" "SseError hx-on event name not correct"
}
]
]
open Giraffe.ViewEngine
/// Pipe-able assertion for a rendered node
let shouldRender expected node =
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
/// Tests for the HtmaxAttrs module
let attributes =
testList "Attributes" [
test "_hxBrowserIndicator succeeds" {
a [ _hxBrowserIndicator ] [] |> shouldRender """<a hx-browser-indicator="true"></a>"""
}
test "_hxLive succeeds" {
main [ _hxLive "q().doStuff()" ] [] |> shouldRender """<main hx-live="q().doStuff()"></main>"""
}
test "_hxOnMax succeeds" {
body [ _hxOnMax SseClose "alert(done)" ] []
|> shouldRender """<body hx-on:htmx:sse:close="alert(done)"></body>"""
}
testList "_hxPreload" [
test "succeeds when given an event" {
blockquote [ _hxPreload (Some "focus") ] []
|> shouldRender """<blockquote hx-preload="focus"></blockquote>"""
}
test "succeeds when not given an event" {
em [ _hxPreload None ] [] |> shouldRender """<em hx-preload></em>"""
}
]
test "_hxSseClose succeeds" {
form [ _hxSseClose "fin" ] [] |> shouldRender """<form hx-sse:close="fin"></form>"""
}
test "_hxSseConnect succeeds" {
div [ _hxSseConnect "/path/to/resource" ] []
|> shouldRender """<div hx-sse:connect="/path/to/resource"></div>"""
}
test "_hxTargets succeeds" {
dl [ _hxTargets ".ephemeral" ] [] |> shouldRender """<dl hx-targets=".ephemeral"></dl>"""
}
test "_hxWsConnect succeeds" {
p [ _hxWsConnect "/over/here" ] [] |> shouldRender """<p hx-ws:connect="/over/here"></p>"""
}
testList "_hxWsSend" [
test "succeeds when given a URL" {
span [ _hxWsSend (Some "/a/new/place") ] []
|> shouldRender """<span hx-ws:send="/a/new/place"></span>"""
}
test "succeeds when not given a URL" {
aside [ _hxWsSend None ] [] |> shouldRender """<aside hx-ws:send></aside>"""
}
]
]
let allTests =
testList "ViewEngine.Htmax" [
hxBrowserIndicatorConfigItem
hxPreloadConfigItem
hxSseWsConfigItem
hxConfig
hxEvent
attributes
]
@@ -9,13 +9,14 @@
<ItemGroup> <ItemGroup>
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<Compile Include="Htmax.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" /> <None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" />
<PackageReference Update="FSharp.Core" Version="6.0.0" /> <PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+243
View File
@@ -0,0 +1,243 @@
/// <summary>Types and functions supporting htmax-bundled extension attributes in Giraffe View Engine</summary>
module Giraffe.ViewEngine.Htmax
/// <summary>Items which can be configured for the <c>hx-browser-indicator</c> extension</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#boosted-elements">Documentation</seealso>
type HxBrowserIndicatorConfigItem =
/// <summary>Whether to show the browser's native loading indicator for <c>hx-boost</c>ed links</summary>
| BoostBrowserIndicator of bool
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| BoostBrowserIndicator it -> "boostBrowserIndicator:" + (string it).ToLowerInvariant()
/// <summary>Items which can be configured for the <c>hx-preload</c> extension</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-preload#configuration">Documentation</seealso>
type HxPreloadConfigItem =
/// <summary>Whether items subject to <c>hx-boost</c> are automatically preloaded (default <c>true</c>)</summary>
| AutoBoost of bool
/// <summary>The event to use for <c>hx-boost</c> auto-preloaded content (default "mousedown")</summary>
| BoostEvent of string
/// <summary>The timeout (in ms) for <c>hx-boost</c> auto-preloaded requests (default 50_000)</summary>
| BoostTimeout of int
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| AutoBoost it -> "autoBoost:" + (string it).ToLowerInvariant()
| BoostEvent it -> $"boostEvent:{it}"
| BoostTimeout it -> $"boostTimeout:{it}"
/// <summary>Items which can be configured for the <c>hx-sse</c> or <c>hx-ws</c> extensions</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#configuration">SSE Documentation</seealso>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#configuration">WebSockets Documentation</seealso>
type HxSseWsConfigItem =
/// <summary>Whether to automatically reconnect on stream end (default <c>true</c>)</summary>
| Reconnect of bool
/// <summary>Delay for reconnect attempts (ms, or numbers with units "1s", "2m"; default "500")</summary>
| ReconnectDelay of string
/// <summary>Maximum delay for reconnect attempts (ms, or numbers with units "1s, "2m"; default "60000")</summary>
| ReconnectMaxDelay of string
/// <summary>Reconnect maximum attempts (use -1 for Infinity; default Infinity)</summary>
| ReconnectMaxAttempts of int
/// <summary>Jitter to use when reconnecting (decimal between 0.0 and 1.0; default 0.3)</summary>
| ReconnectJitter of double
/// <summary>Whether to pause when the current tab is in the background (default <c>true</c>)</summary>
| PauseOnBackground of bool
/// <summary>Time-to-Live (TTL) for pending requests (<c>hx-ws</c> only)</summary>
| PendingRequestTtl of int
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| Reconnect it -> "reconnect:" + (string it).ToLowerInvariant()
| ReconnectDelay it -> $"reconnectDelay:{it}"
| ReconnectMaxDelay it -> $"reconnectMaxDelay:{it}"
| ReconnectMaxAttempts it -> "reconnectMaxAttempts:" + if it = -1 then "Infinity" else string it
| ReconnectJitter it -> $"reconnectJitter:{it}"
| PauseOnBackground it -> "pauseOnBackground:" + (string it).ToLowerInvariant()
| PendingRequestTtl it -> $"pendingRequestTTL:{it}"
/// <summary>Helpers for generating extension configuration parameters</summary>
[<AutoOpen>]
module HxConfig =
/// Create an HCON section suitable for hx-config
let private hconSettings title values =
values |> Seq.map (sprintf "%s.%s" title) |> String.concat " "
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxBrowserIndicatorConfig (items: HxBrowserIndicatorConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "browser-indicator"
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxPreloadConfig (items: HxPreloadConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "preload"
/// <summary>Generate configuration items for SSE suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxSseConfig (items: HxSseWsConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "sse"
/// <summary>Generate configuration items for WebSockets suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxWsConfig (items: HxSseWsConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "ws"
/// <summary>The events recognized by htmax-bundled extensions</summary>
[<Struct>]
type HxEvent =
/// <summary>Fired after an SSE connection is established</summary>
| AfterSseConnection
/// <summary>Fired after an SSE message is received</summary>
| AfterSseMessage
/// <summary>Fired after a WebSocket connection is established</summary>
| AfterWsConnection
/// <summary>Fired after a WebSocket message is received</summary>
| AfterWsMessage
/// <summary>Fired after a WebSocket request is sent</summary>
| AfterWsRequest
/// <summary>Fired before an SSE connection is established (cancelable)</summary>
| BeforeSseConnection
/// <summary>Fired before a received SSE message is processed (cancelable)</summary>
| BeforeSseMessage
/// <summary>Fired before a WebSocket connection is established (cancelable)</summary>
| BeforeWsConnection
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
| DownloadComplete
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
| DownloadProgress
/// <summary>Fired when a chunk is received for a download handled by <c>hx-download</c></summary>
| DownloadStart
/// <summary>Fired before a received WebSocket message is processed (cancelable)</summary>
| BeforeWsMessage
/// <summary>Fired before a WebSocket request is sent (cancelable)</summary>
| BeforeWsRequest
/// <summary>Fired when an SSE connection is closed</summary>
| SseClose
/// <summary>Fired when an SSE connection errors</summary>
| SseError
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
static member private Values = Map [
AfterSseConnection, ("afterSseConnection", "after:sse:connection")
AfterSseMessage, ("afterSseMessage", "after:sse:message")
AfterWsConnection, ("afterWsConnection", "after:ws:connection")
AfterWsMessage, ("afterWsMessage", "after:ws:message")
AfterWsRequest, ("afterWsRequest", "after:ws:request")
BeforeSseConnection, ("beforeSseConnection", "before:sse:connection")
BeforeSseMessage, ("beforeSseMessage", "before:sse:message")
BeforeWsConnection, ("beforeWsConnection", "before:ws:connection")
BeforeWsMessage, ("beforeWsMessage", "before:ws:message")
BeforeWsRequest, ("beforeWsRequest", "before:ws:request")
DownloadComplete, ("downloadComplete", "download:complete")
DownloadProgress, ("downloadProgress", "download:progress")
DownloadStart, ("downloadStart", "download:start")
SseClose, ("sseClose", "sse:close")
SseError, ("sseError", "sse:error")
]
/// <summary>The htmx event name</summary>
override this.ToString() = fst HxEvent.Values[this]
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary>
member this.ToHxOnString() = snd HxEvent.Values[this]
/// <summary>Attributes and flags for htmax-bundled extensions</summary>
[<AutoOpen>]
module HtmaxAttrs =
/// <summary>Display the browser's native loading indicator when an htmx request is in flight</summary>
/// <returns>A configured <c>hx-browser-indicator</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#usage">Documentation</seealso>
let _hxBrowserIndicator =
attr "hx-browser-indicator" "true"
/// <summary>Run script each time the DOM changes</summary>
/// <param name="script">The script to be run each time the DOM changes (may use <c>q</c> helper)</param>
/// <returns>A configured <c>hx-live</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-live#the-hx-live-attribute">Documentation</seealso>
let _hxLive script =
attr "hx-live" script
/// <summary>Generate an <c>hx-on:htmx</c> event for an htmax-bundled extension event</summary>
/// <param name="event">The <c>HxEvent</c> to be handled</param>
/// <param name="handler">The script to be executed when the event occurs</param>
/// <returns>A configured <c>hx-on:htmx:</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
let _hxOnMax (event: HxEvent) handler =
Htmx.HtmxAttrs._hxOnEvent $"htmx:{event.ToHxOnString()}" handler
/// <summary>Identify a resource as one that should be preloaded</summary>
/// <param name="evt">The DOM event or htmx trigger which should cause the preload (optional; default behavior is
/// preloading on <c>mousedown</c>)</param>
/// <returns>A configured <c>hx-preload</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-preload#usage">Documentation</seealso>
let _hxPreload evt =
match evt with Some it -> attr "hx-preload" it | None -> flag "hx-preload"
/// <summary>Define an SSE message which indicates the connection should be closed</summary>
/// <param name="message">The text of the message which indicates the SSE connection should be closed</param>
/// <returns>A configured <c>hx-sse:close</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseclose">Documentation</seealso>
let _hxSseClose message =
attr "hx-sse:close" message
/// <summary>Define a URL to which an SSE connection should be established</summary>
/// <param name="url">The URL to which an SSE connection should be established</param>
/// <returns>A configured <c>hx-sse:connect</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseconnect">Documentation</seealso>
let _hxSseConnect url =
attr "hx-sse:connect" url
/// <summary>Replace multiple targets with the same content</summary>
/// <param name="selector">The CSS selector to identify targets for the replacement</param>
/// <returns>A configured <c>hx-targets</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-targets#usage">Documentation</seealso>
let _hxTargets selector =
attr "hx-targets" selector
/// <summary>Connect to a WebSocket URL</summary>
/// <param name="url">The URL to which a WebSocket connection should be established</param>
/// <returns>A configured <c>hx-ws:connect</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
let _hxWsConnect url =
attr "hx-ws:connect" url
/// <summary>Send data via a WebSocket connection</summary>
/// <param name="url">The URL to which the data should be sent (optional; default is connection URL)</param>
/// <returns>A configured <c>hx-ws:send</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
let _hxWsSend url =
match url with Some it -> attr "hx-ws:send" it | None -> flag "hx-ws:send"
+98 -35
View File
@@ -71,8 +71,11 @@ type HxEvent =
/// <summary>Triggered after an AJAX request has completed processing a successful response</summary> /// <summary>Triggered after an AJAX request has completed processing a successful response</summary>
| [<System.Obsolete "Removed in v4; use AfterInit">] AfterOnLoad | [<System.Obsolete "Removed in v4; use AfterInit">] AfterOnLoad
/// <summary>Triggered after htmx has initialized a DOM node or subtree</summary>
| AfterProcess
/// <summary>Triggered after htmx has initialized a node</summary> /// <summary>Triggered after htmx has initialized a node</summary>
| [<System.Obsolete "Removed in v4; use AfterInit">] AfterProcessNode | [<System.Obsolete "Removed in v4; use AfterProcess">] AfterProcessNode
/// <summary>Triggered after new content is saved to the history cache</summary> /// <summary>Triggered after new content is saved to the history cache</summary>
| AfterPushIntoHistory | AfterPushIntoHistory
@@ -84,13 +87,13 @@ type HxEvent =
| AfterRequest | AfterRequest
/// <summary>Triggered after the DOM has settled</summary> /// <summary>Triggered after the DOM has settled</summary>
| [<System.Obsolete "Removed in v4; use AfterSwap">] AfterSettle | AfterSettle
/// <summary>Triggered after a Server Sent Events (SSE) message is read</summary> /// <summary>Triggered after a Server Sent Events (SSE) message is read</summary>
| AfterSseMessage | [<System.Obsolete "Removed in v4; use Htmax.HxEvents.AfterSseMessage">] AfterSseMessage
/// <summary>Triggered after a Server Sent Events (SSE) stream is closed</summary> /// <summary>Triggered after a Server Sent Events (SSE) stream is closed</summary>
| AfterSseStream | [<System.Obsolete "Removed in v4; SSE is assumed to be a stream">] AfterSseStream
/// <summary>Triggered after new content has been swapped in</summary> /// <summary>Triggered after new content has been swapped in</summary>
| AfterSwap | AfterSwap
@@ -116,12 +119,18 @@ type HxEvent =
/// <summary>Triggered before any response processing occurs</summary> /// <summary>Triggered before any response processing occurs</summary>
| [<System.Obsolete "Removed in v4; use BeforeInit">] BeforeOnLoad | [<System.Obsolete "Removed in v4; use BeforeInit">] BeforeOnLoad
/// <summary>Triggered before htmx begins processing a DOM node or subtree</summary>
| BeforeProcess
/// <summary>Triggered before htmx initializes a node</summary> /// <summary>Triggered before htmx initializes a node</summary>
| [<System.Obsolete "Removed in v4; use BeforeInit">] BeforeProcessNode | [<System.Obsolete "Removed in v4; use BeforeProcess">] BeforeProcessNode
/// <summary>Triggered before an HTTP request is made</summary> /// <summary>Triggered before an HTTP request is made</summary>
| BeforeRequest | BeforeRequest
/// <summary>Triggered before an HTTP response is processed (cancelable)</summary>
| BeforeResponse
/// <summary>Triggered before a history restore request is made</summary> /// <summary>Triggered before a history restore request is made</summary>
| BeforeRestoreHistory | BeforeRestoreHistory
@@ -129,13 +138,13 @@ type HxEvent =
| [<System.Obsolete "Removed in v4; use BeforeRequest">] BeforeSend | [<System.Obsolete "Removed in v4; use BeforeRequest">] BeforeSend
/// <summary>Triggered before a Server Sent Events (SSE) message is read</summary> /// <summary>Triggered before a Server Sent Events (SSE) message is read</summary>
| BeforeSseMessage | [<System.Obsolete "Removed in v4; use Htmax.HxEvents.BeforeSseMessage">] BeforeSseMessage
/// <summary>Triggered before a Server Sent Events (SSE) connection is reconnected</summary> /// <summary>Triggered before a Server Sent Events (SSE) connection is reconnected</summary>
| BeforeSseReconnect | [<System.Obsolete "Removed in v4; use fetch event handlers">] BeforeSseReconnect
/// <summary>Triggered before a Server Sent Events (SSE) stream is opened</summary> /// <summary>Triggered before a Server Sent Events (SSE) stream is opened</summary>
| BeforeSseStream | [<System.Obsolete "Removed in v4; SSE is assumed to be a stream">] BeforeSseStream
/// <summary>Triggered before a swap is done, allows you to configure the swap</summary> /// <summary>Triggered before a swap is done, allows you to configure the swap</summary>
| BeforeSwap | BeforeSwap
@@ -199,16 +208,16 @@ type HxEvent =
| [<System.Obsolete "Removed in v4; use AfterPushIntoHistory (or AfterHistoryUpdate to catch new and updated content)">] PushedIntoHistory | [<System.Obsolete "Removed in v4; use AfterPushIntoHistory (or AfterHistoryUpdate to catch new and updated content)">] PushedIntoHistory
/// <summary>Triggered when an HTTP response error (non-200 or 300 response code) occurs</summary> /// <summary>Triggered when an HTTP response error (non-200 or 300 response code) occurs</summary>
| [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] ResponseError | ResponseError
/// <summary>Triggered when a network error prevents an HTTP request from happening</summary> /// <summary>Triggered when a network error prevents an HTTP request from happening</summary>
| [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] SendError | [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] SendError
/// <summary>Triggered when an error occurs with a SSE source</summary> /// <summary>Triggered when an error occurs with a SSE source</summary>
| [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] SseError | [<System.Obsolete "Removed in v4; use Htmax.HxEvents.SseError)">] SseError
/// <summary>Triggered when an SSE source is opened</summary> /// <summary>Triggered when an SSE source is opened</summary>
| [<System.Obsolete "Removed in v4; use fetch event listener">] SseOpen | [<System.Obsolete "Removed in v4; use Htmax.HxEvents.AfterSseConnection">] SseOpen
/// <summary>Triggered when an error occurs during the swap phase</summary> /// <summary>Triggered when an error occurs during the swap phase</summary>
| [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] SwapError | [<System.Obsolete "Removed in v4; use Error (all errors consolidated)">] SwapError
@@ -229,10 +238,10 @@ type HxEvent =
| [<System.Obsolete "Removed in v4; use native validation API">] ValidationHalted | [<System.Obsolete "Removed in v4; use native validation API">] ValidationHalted
/// <summary>Triggered when an ajax request aborts</summary> /// <summary>Triggered when an ajax request aborts</summary>
| [<System.Obsolete "Removed in v4; use fetch event listeners">] XhrAbort | [<System.Obsolete "Removed in v4; use fetch event listeners or Error event">] XhrAbort
/// <summary>Triggered when an ajax request ends</summary> /// <summary>Triggered when an ajax request ends</summary>
| [<System.Obsolete "Removed in v4; use fetch event listeners">] XhrLoadEnd | [<System.Obsolete "Removed in v4; use fetch event listeners or FinallyRequest event">] XhrLoadEnd
/// <summary>Triggered when an ajax request starts</summary> /// <summary>Triggered when an ajax request starts</summary>
| [<System.Obsolete "Removed in v4; use fetch event listeners">] XhrLoadStart | [<System.Obsolete "Removed in v4; use fetch event listeners">] XhrLoadStart
@@ -247,11 +256,11 @@ type HxEvent =
AfterHistoryUpdate, ("afterHistoryUpdate", "after:history:update") AfterHistoryUpdate, ("afterHistoryUpdate", "after:history:update")
AfterInit, ("afterInit", "after:init") AfterInit, ("afterInit", "after:init")
AfterOnLoad, ("afterOnLoad", "after:init") AfterOnLoad, ("afterOnLoad", "after:init")
AfterProcessNode, ("afterProcessNode", "after:init") AfterProcessNode, ("afterProcessNode", "after:process")
AfterPushIntoHistory, ("afterPushIntoHistory", "after:push:into:history") AfterPushIntoHistory, ("afterPushIntoHistory", "after:push:into:history")
AfterReplaceIntoHistory, ("afterReplaceIntoHistory", "after:replace:into:history") AfterReplaceIntoHistory, ("afterReplaceIntoHistory", "after:replace:into:history")
AfterRequest, ("afterRequest", "after:request") AfterRequest, ("afterRequest", "after:request")
AfterSettle, ("afterSettle", "after:swap") AfterSettle, ("afterSettle", "after:settle")
AfterSseMessage, ("afterSseMessage", "after:sse:message") AfterSseMessage, ("afterSseMessage", "after:sse:message")
AfterSseStream, ("afterSseStream", "after:sse:stream") AfterSseStream, ("afterSseStream", "after:sse:stream")
AfterSwap, ("afterSwap", "after:swap") AfterSwap, ("afterSwap", "after:swap")
@@ -262,9 +271,11 @@ type HxEvent =
BeforeHistoryUpdate, ("beforeHistoryUpdate", "before:history:update") BeforeHistoryUpdate, ("beforeHistoryUpdate", "before:history:update")
BeforeInit, ("beforeInit", "before:init") BeforeInit, ("beforeInit", "before:init")
BeforeOnLoad, ("beforeOnLoad", "before:init") BeforeOnLoad, ("beforeOnLoad", "before:init")
BeforeProcessNode, ("beforeProcessNode", "before:init") BeforeProcess, ("beforeProcess", "before:process")
BeforeRestoreHistory, ("beforeRestoreHistory", "before:restore:history") BeforeProcessNode, ("beforeProcessNode", "before:process")
BeforeRequest, ("beforeRequest", "before:request") BeforeRequest, ("beforeRequest", "before:request")
BeforeResponse, ("beforeResponse", "before:response")
BeforeRestoreHistory, ("beforeRestoreHistory", "before:restore:history")
BeforeSend, ("beforeSend", "before:request") BeforeSend, ("beforeSend", "before:request")
BeforeSseMessage, ("beforeSseMessage", "before:sse:message") BeforeSseMessage, ("beforeSseMessage", "before:sse:message")
BeforeSseReconnect, ("beforeSseReconnect", "before:sse:reconnect") BeforeSseReconnect, ("beforeSseReconnect", "before:sse:reconnect")
@@ -288,18 +299,18 @@ type HxEvent =
OobErrorNoTarget, ("oobErrorNoTarget", "error") OobErrorNoTarget, ("oobErrorNoTarget", "error")
Prompt, ("prompt", "prompt") Prompt, ("prompt", "prompt")
PushedIntoHistory, ("pushedIntoHistory", "after:push:into:history") PushedIntoHistory, ("pushedIntoHistory", "after:push:into:history")
ResponseError, ("responseError", "error") ResponseError, ("responseError", "response:error")
SendError, ("sendError", "error") SendError, ("sendError", "error")
SseError, ("sseError", "error") SseError, ("sseError", "sse:error")
SseOpen, ("sseOpen", "sse-open") SseOpen, ("sseOpen", "after:sse:connection")
SwapError, ("swapError", "error") SwapError, ("swapError", "error")
TargetError, ("targetError", "error") TargetError, ("targetError", "error")
Timeout, ("timeout", "error") Timeout, ("timeout", "error")
ValidationValidate, ("validation:validate", "validation:validate") ValidationValidate, ("validation:validate", "validation:validate")
ValidationFailed, ("validation:failed", "validation:failed") ValidationFailed, ("validation:failed", "validation:failed")
ValidationHalted, ("validation:halted", "validation:halted") ValidationHalted, ("validation:halted", "validation:halted")
XhrAbort, ("xhr:abort", "xhr:abort") XhrAbort, ("xhr:abort", "error")
XhrLoadEnd, ("xhr:loadend", "xhr:loadend") XhrLoadEnd, ("xhr:loadend", "finally:request")
XhrLoadStart, ("xhr:loadstart", "xhr:loadstart") XhrLoadStart, ("xhr:loadstart", "xhr:loadstart")
XhrProgress, ("xhr:progress", "xhr:progress") XhrProgress, ("xhr:progress", "xhr:progress")
] ]
@@ -610,12 +621,14 @@ module HtmxAttrs =
/// <summary>The URL which will receive the request (use with <c>_hxMethod</c>)</summary> /// <summary>The URL which will receive the request (use with <c>_hxMethod</c>)</summary>
/// <param name="url">The URL for the attribute</param> /// <param name="url">The URL for the attribute</param>
/// <seealso href="https://four.htmx.org/attributes/hx-action/">Documentation</seealso> /// <seealso href="https://four.htmx.org/attributes/hx-action/">Documentation</seealso>
let _hxAction url = attr "hx-action" url let _hxAction url =
attr "hx-action" url
/// <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 <c>_hxNoBoost</c> to set to false</remarks>
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso> /// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
let _hxBoost = attr "hx-boost" "true" let _hxBoost =
attr "hx-boost" "true"
/// <summary>Configure request behavior</summary> /// <summary>Configure request behavior</summary>
/// <param name="config">The configuration parameters to use for the request</param> /// <param name="config">The configuration parameters to use for the request</param>
@@ -753,11 +766,12 @@ module HtmxAttrs =
/// <summary>Attach an event handler for htmx events</summary> /// <summary>Attach an event handler for htmx events</summary>
/// <param name="hxEvent">The <c>HxEvent</c> to be handled</param> /// <param name="hxEvent">The <c>HxEvent</c> to be handled</param>
/// <param name="handler">The script to be executed when the event occurs</param> /// <param name="handler">The script to be executed when the event occurs</param>
/// <returns>A configured <c>hx-on::</c> attribute</returns> /// <returns>A configured <c>hx-on:htmx:</c> attribute</returns>
/// <remarks>For events defined in htmax, use <c>_hxOnMax</c></remarks>
/// <seealso cref="HxEvent" /> /// <seealso cref="HxEvent" />
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso> /// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
let _hxOnHxEvent (hxEvent: HxEvent) handler = let _hxOnHxEvent (hxEvent: HxEvent) handler =
_hxOnEvent $":{hxEvent.ToHxOnString()}" handler _hxOnEvent $"htmx:{hxEvent.ToHxOnString()}" handler
/// <summary>Filters the parameters that will be submitted with a request</summary> /// <summary>Filters the parameters that will be submitted with a request</summary>
/// <param name="toInclude">The fields to include (use <c>HxParams</c> to generate this value)</param> /// <param name="toInclude">The fields to include (use <c>HxParams</c> to generate this value)</param>
@@ -790,9 +804,13 @@ module HtmxAttrs =
/// <summary>Shows a <c>prompt()</c> dialog before submitting a request</summary> /// <summary>Shows a <c>prompt()</c> dialog before submitting a request</summary>
/// <param name="text">The text for the prompt</param> /// <param name="text">The text for the prompt</param>
/// <returns>A configured <c>hx-prompt</c> attribute</returns> /// <returns>A configured <c>hx-prompt</c> attribute</returns>
/// <remarks>The value provided will be in the <c>HX-Prompt</c> request header</remarks> /// <remarks>
/// <ul>
/// <li><b>NEW IN v4:</b> This functionality is dependent on the hx-prompt extension being loaded</li>
/// <li>The value provided will be in the <c>HX-Prompt</c> request header</li>
/// </ul>
/// </remarks>
/// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso> /// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso>
[<System.Obsolete "hx-prompt removed in v4; use hx-confirm with async JavaScript function">]
let _hxPrompt text = let _hxPrompt text =
attr "hx-prompt" text attr "hx-prompt" text
@@ -852,6 +870,31 @@ module HtmxAttrs =
let _hxSelectOob selectors = let _hxSelectOob selectors =
attr "hx-select-oob" selectors attr "hx-select-oob" selectors
/// <summary>Define a response to an HTTP status code or code range</summary>
/// <param name="code">
/// The code or code range; for a code, pass the full code as a string (ex. <c>404</c>), and for a range, pass a
/// string with one or two characters (ex. <c>5</c> for all 500s, <c>42</c> for 420-429)
/// </param>
/// <param name="swap">The swap style to use for this action(use <c>HxSwap</c> values)</param>
/// <param name="action">
/// The action which should be taken for responses matching the status code; can be a combination of the following,
/// with each item separated by a space:
/// <ul>
/// <li><c>target:</c> and a CSS selector for the swap target (overrides original <c>hx-target</c>)</li>
/// <li><c>select:</c> and a CSS selector for the content to be swapped (overrides original <c>hx-select</c>)</li>
/// <li><c>push:</c> and <c>true</c>, <c>false</c>, or a URL; <c>false</c> does not update URL history, <c>true</c>
/// updates with the current URL, and a string URL will be pushed into history as-is</li>
/// <li><c>replace:</c> and <c>true</c>, <c>false</c>, or a URL; same as <c>push:</c>, but replaces in history
/// instead</li>
/// <li><c>transition:</c> and <c>true</c> or <c>false</c>; whether to use view transitions</li>
/// </ul>
/// If only the type of swap is being changed, pass an empty string to this parameter.
/// </param>
/// <seealso href="https://four.htmx.org/reference/attributes/hx-status">Documentation</seealso>
let _hxStatus code swap action =
let range = $"%s{code}xx".Substring(0, 3)
attr $"hx-status:{range}" ($"swap:%s{swap} %s{action}".Trim())
/// <summary> /// <summary>
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>) /// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>)
/// </summary> /// </summary>
@@ -927,14 +970,14 @@ module HtmxAttrs =
/// <param name="url">The URL from which events will be received</param> /// <param name="url">The URL from which events will be received</param>
/// <returns>A configured <c>sse-connect</c> attribute</returns> /// <returns>A configured <c>sse-connect</c> attribute</returns>
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso> /// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
let _sseConnect url = let [<System.Obsolete "Removed in v4; use Htmax._hxSseConnect">] _sseConnect url =
attr "sse-connect" url attr "sse-connect" url
/// <summary>The name(s) of the message(s) to swap into the DOM</summary> /// <summary>The name(s) of the message(s) to swap into the DOM</summary>
/// <param name="messages">The message names (comma-delimited) to swap (use "message" for unnamed events)</param> /// <param name="messages">The message names (comma-delimited) to swap (use "message" for unnamed events)</param>
/// <returns>A configured <c>sse-swap</c> attribute</returns> /// <returns>A configured <c>sse-swap</c> attribute</returns>
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso> /// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
let _sseSwap messages = let [<System.Obsolete "Removed in v4; use hx-swap on target element">] _sseSwap messages =
attr "sse-swap" messages attr "sse-swap" messages
@@ -973,22 +1016,42 @@ module Script =
open System open System
/// <summary>Script tag to load the package-provided version of htmx</summary> /// <summary>Script tag to load the package-provided version of htmx</summary>
let local = script [ _src htmxLocalScript ] [] let local = script [ _src StaticAssetUrl.htmx ] []
/// <summary>Script tag to load the minified version from jsdelivr.net</summary> /// <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> /// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnMinified = let cdnMinified =
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha6/dist/htmx.min.js" script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js"
_integrity "sha384-/5n21KGM472oBhvzUrvju8FRDq/4WNMS3TGw5RWFkZR/kq+sCevqNXFcakqRtaHu" _integrity "sha384-5dnhUXCt1hXGvYrjAnKwgNX3I8xtIJiW6eIHIbeo7oWyXv2XpWYC/rl+ZiWfuYO5"
_crossorigin "anonymous" ] [] _crossorigin "anonymous" ] []
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary> /// <summary>Script tag to load the unminified version from jsdelivr.net</summary>
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks> /// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnUnminified = let cdnUnminified =
script [ _src "https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-alpha6/dist/htmx.js" script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js"
_integrity "sha384-I3uRy2RjSNcS8Oi2SNWZD9IrafyrrrBb4QaHNA9pWVWfJuKtL0p8qAwHSrEyXoKh" _integrity "sha384-RZoQSZlu2BAuZMuM5lTKAWXXSKC+7X6eVzP1pwkUBcyfPmOswexqVOsUqQMKbAFA"
_crossorigin "anonymous" ] [] _crossorigin "anonymous" ] []
/// <summary>Script tags to load the htmax bundle</summary>
module Max =
/// <summary>Script tag to load the package-provided version of the htmx-plus-extensions bundle</summary>
let local = script [ _src StaticAssetUrl.htmax ] []
/// <summary>Script tag to load the minified htmx-plus-extensions bundle 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@{HtmxVersion}/dist/htmax.min.js"
_integrity "sha384-VVbrNR6a+H8puV17ZlJ8aUUMTgbcDiqM1vlLYEmaaU4oFANllvTK2pLynNonElP+"
_crossorigin "anonymous" ] []
/// <summary>Script tag to load the unminified htmx-plus-extensions bundle from jsdelivr.net</summary>
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
let cdnUnminified =
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.js"
_integrity "sha384-kjhVuvnX3/TsR1qH4JaIcHR6muh/WLMU5CTQRacCQZERzQlP4/r9p/TK7ucFwqvV"
_crossorigin "anonymous" ] []
/// <summary>Script tag to load the minified version from jsdelivr.net</summary> /// <summary>Script tag to load the minified version from jsdelivr.net</summary>
[<Obsolete "Use cdnMinified instead; this will be removed in v4">] [<Obsolete "Use cdnMinified instead; this will be removed in v4">]
let minified = cdnMinified let minified = cdnMinified
+9 -6
View File
@@ -2,9 +2,9 @@
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: 4.0.0-alpha6** **htmx version: 4.0.0-beta5**
_Upgrading from v2.x: see [the migration guide](https://four.htmx.org/migration-guide-htmx-4/) for changes, which are plentiful. htmx switches from `XMLHTTPRequest` to `fetch`, and many changes are related to the new event cycle._ _Upgrading from v2.x: see [the migration guide](https://four.htmx.org/docs/get-started/migration) for changes, which are plentiful. htmx switches from `XMLHTTPRequest` to `fetch`, and many changes are related to the new event cycle._
_Inheritance is now explicit; to have an attribute's value inherited to its children, wrap the attribute in `hxInherited` (ex. `hxInherited (_hxTarget "#main")`). Values can be appended to inherited values as well using the `hxAppend` modifier._ _Inheritance is now explicit; to have an attribute's value inherited to its children, wrap the attribute in `hxInherited` (ex. `hxInherited (_hxTarget "#main")`). Values can be appended to inherited values as well using the `hxAppend` modifier._
@@ -13,7 +13,8 @@ _Several constructs have been marked obsolete in this release, and will be remov
### Setup ### Setup
1. Install the package. 1. Install the package.
2. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`. 1. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`.
1. If the client is using the `htmax` bundle, also `open Giraffe.ViewEngine.Htmax`.
### Use ### Use
@@ -34,7 +35,9 @@ 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`. `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). htmx v4 also distributes a "max" bundle which contains some common extensions; these are available with the same names under `Htmx.Script.Max`.
_NOTE: When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`._
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).
@@ -47,10 +50,10 @@ htmx's attributes and these attribute functions map one-to-one. There are two ex
The htmx `hx-on` attribute supports multiple events if they are separated with a newline (`\n`) character. The value provided to this attribute will be attribute-escaped, but in testing, it was interpreted correctly. The htmx `hx-on` attribute supports multiple events if they are separated with a newline (`\n`) character. The value provided to this attribute will be attribute-escaped, but in testing, it was interpreted correctly.
The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though: The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though:
- `HxRequest` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request. - `HxConfig` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request.
```fsharp ```fsharp
HxRequest.Configure [ HxRequest.Timeout 500 ] |> _hxRequest HxConfig.Configure [ HxRequest.Timeout 500 ] |> _hxConfig
``` ```
- `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing: - `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing: