Compare commits

...

45 Commits
v1.6.1 ... main

Author SHA1 Message Date
f0de18845f Update script tags and package version to 2.0.2 2024-08-12 21:36:47 -04:00
961307fd99 Update htmx script to 2.0.1 2024-07-29 18:06:19 -04:00
a2960a79c6 Add pack script; drop beta tag 2024-06-18 19:38:28 -04:00
541384a92f htmx v2
- Drop `hx-sse` and `hx-ws` attributes
- Drop explicit .NET 7 support
- Update deps to latest versions
2024-06-18 19:16:10 -04:00
8cb5d6bfa7 Merge pull request 'htmx v2-beta4' (#12) from v2-beta4 into main
Reviewed-on: #12
2024-05-23 23:30:58 +00:00
1a11e3511a Update htmx to v2.0.0-beta4 2024-05-23 19:26:00 -04:00
32e962416d Migrate PR 10 from GitHub
https://github.com/bit-badger/Giraffe.Htmx/pull/10
2024-05-23 19:19:07 -04:00
29839fa795 Update script tags for v2.0.0 beta3
- Update paths in package metadata
2024-04-17 22:25:06 -04:00
7f9b3a6234 Merge branch 'htmx-version-2' 2024-04-17 22:06:23 -04:00
a8d2b819dc Merge branch 'htmx-version-2' of https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx into htmx-version-2 2024-04-17 22:04:21 -04:00
1ea05b79ed Update for htmx v1.9.11 2024-03-18 20:14:51 -04:00
4f6bb8367a Update for v2 beta 1 2024-03-18 20:03:18 -04:00
b3665a4b72
Update CI to Node 20-based versions 2024-02-12 18:29:27 -05:00
bdb7255a1c Update for v2 alpha 2 2024-02-12 17:45:16 -05:00
9276db7ffe
Sync changes from main 2024-01-30 23:04:19 -05:00
94b68f76c9
Add workflow dispatch to CI 2024-01-30 23:02:45 -05:00
90de16529c
Move packaging to .NET 8 2024-01-30 22:58:45 -05:00
452f15b2d4 Add alpha tag 2024-01-30 22:55:36 -05:00
59246ae7f5
Add v2 branch to CI 2024-01-30 22:48:08 -05:00
16355e8f58 Add v2.0.0-alpha1 support 2024-01-30 22:46:34 -05:00
71286b9064 Move FSharp.Core version to .fsproj files
- Change event DU to struct
2024-01-02 19:26:09 -05:00
85ac22877c
v1.9.10 (#9) 2024-01-02 19:02:24 -05:00
dea2323499 Update NuGet READMEs 2023-11-22 16:13:57 -05:00
fcaaa693bc Bump htmx version; add .NET 8 support 2023-11-22 16:09:52 -05:00
8fd6af8c26 Add support for hx-disabled-elt
- Update script tag to 1.9.6
2023-09-27 20:31:56 -04:00
69a4034661 Update to htmx 1.9.5 2023-08-25 12:36:30 -04:00
5aa9408e60 Update to htmx 1.9.4 2023-07-26 12:54:48 -04:00
98f53a5e53 Add HX-Reselect; update for 1.9.3 2023-07-15 17:54:01 -04:00
7798314fb8 Update script tags for v1.9.2 2023-05-06 13:14:58 -04:00
50c66435e8 Add _hxOn and _hxSwapWithTransition
- Update scripts and version to 1.9.0
2023-04-14 16:56:08 -04:00
5b3a1be87e Update script for 1.8.6
- Sync version in package READMEs
2023-03-03 12:26:40 -05:00
168f434030
Remove PostgreSQL verbiage
oops
2023-02-27 20:56:49 -05:00
e3db0bced0
Xunit -> Expecto (#7) 2023-02-27 20:55:16 -05:00
fb5c4f1bcb
Change to main branch 2023-02-27 20:51:51 -05:00
c3229d51d1
Remove manual start 2023-02-27 20:42:42 -05:00
951edef8ad
Allow manual starting 2023-02-27 20:41:25 -05:00
3908451d6e
Add CI action 2023-02-27 20:38:33 -05:00
277d93dd99 Add hx-history attribute
Add .NET 7 support
2023-01-17 20:41:54 -05:00
061f6e5a4e Add link to frag render post 2022-11-24 11:34:07 -05:00
bb2df73175 Add _hxValidate, fragment rendering
Also bump version to 1.8.4
2022-11-24 10:50:08 -05:00
e0c567098d Add files to package common project 2022-07-14 11:32:42 -04:00
4be5bad8ef Update package READMEs 2022-07-14 09:24:49 -04:00
a169000ce2
Update for version 1.8.0 (#6)
- Bump library version
- Bump htmx script version
- Add hx-replace-url / HX-Replace-Url
- Add hx-select-oob
- Change hx-push-url to accept a string
- Add HX-Push-Url, obsolete HX-Push
- Implement HX-Reswap header
- Rework project structure to add common project
2022-07-14 09:13:52 -04:00
c587a28770
Update for htmx 1.7.0 (#4)
Fixes #3
2022-02-23 21:54:51 -05:00
b5292bffc4 HTMX -> htmx 2022-01-07 16:02:01 -05:00
27 changed files with 2226 additions and 1148 deletions

57
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,57 @@
name: CI
on:
push:
branches: [ "main", "htmx-version-2" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
jobs:
build-and-test:
runs-on: ubuntu-latest
strategy:
matrix:
dotnet-version: [ "6.0", "7.0", "8.0" ]
steps:
- uses: actions/checkout@v4
- name: Setup .NET ${{ matrix.dotnet-version }}.x
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ matrix.dotnet-version }}.x
- name: Restore dependencies
run: dotnet restore src/Giraffe.Htmx.sln
- name: Build
run: dotnet build src/Giraffe.Htmx.sln --no-restore
- name: Test (.NET ${{ matrix.dotnet-version }})
run: dotnet run --project src/Tests/Tests.fsproj -f net${{ matrix.dotnet-version }}
publish:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0"
- name: Package Common library
run: dotnet pack src/Common/Giraffe.Htmx.Common.fsproj -c Release
- name: Move Common package
run: cp src/Common/bin/Release/Giraffe.Htmx.Common.*.nupkg .
- name: Package Server library
run: dotnet pack src/Htmx/Giraffe.Htmx.fsproj -c Release
- name: Move Server package
run: cp src/Htmx/bin/Release/Giraffe.Htmx.*.nupkg .
- name: Package View Engine library
run: dotnet pack src/ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj -c Release
- name: Move View Engine package
run: cp src/ViewEngine.Htmx/bin/Release/Giraffe.ViewEngine.Htmx.*.nupkg .
- name: Save Packages
uses: actions/upload-artifact@v4
with:
name: packages
path: |
*.nupkg

2
.gitignore vendored
View File

@ -5,3 +5,5 @@
.ionide .ionide
.idea .idea
*.user *.user
.vscode
src/*.nupkg

View File

@ -21,22 +21,23 @@ In addition to the regular HTTP request payloads, htmx sets [one or more headers
A server may want to respond to a request that originated from htmx differently than a regular request. One way htmx can provide the same feel as a Single Page Application (SPA) is by swapping out the `body` content (or an element within it) instead of reloading the entire page. In this case, the developer can provide a partial layout to be used for these responses, while returning the full page for regular requests. The `IsHtmx` property makes this easy... A server may want to respond to a request that originated from htmx differently than a regular request. One way htmx can provide the same feel as a Single Page Application (SPA) is by swapping out the `body` content (or an element within it) instead of reloading the entire page. In this case, the developer can provide a partial layout to be used for these responses, while returning the full page for regular requests. The `IsHtmx` property makes this easy...
```fsharp ```fsharp
// "partial" and "full" are handlers that return the contents; // "partial" and "full" are handlers that return the contents;
// "view" can be whatever your view engine needs for the body of the page // "view" can be whatever your view engine needs for the body of the page
let result view : HttpHandler = let result view : HttpHandler =
fun next ctx -> fun next ctx ->
match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with if ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh then
| true -> partial view partial view
| false -> full view else
full view
``` ```
htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response. htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response.
```fsharp ```fsharp
let theHandler : HttpHandler = let theHandler : HttpHandler =
fun next ctx -> fun next ctx ->
// some interesting stuff // some interesting stuff
withHxRedirect "/the-new-url" >=> Successful.OK withHxRedirect "/the-new-url" >=> Successful.OK
``` ```
Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well. Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well.
@ -48,8 +49,10 @@ As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the
As an example, creating a `div` that loads data once the HTML is rendered: As an example, creating a `div` that loads data once the HTML is rendered:
```fsharp ```fsharp
let autoload = let autoload =
div [ _hxGet "/lazy-load-data"; _hxTrigger "load" ] [ str "Loading..." ] div [ _hxGet "/lazy-load-data"; _hxTrigger HxTrigger.Load ] [
str "Loading..."
]
``` ```
_(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true. To disable it for an element, use `_hxNoBoost` instead.)_ _(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true. To disable it for an element, use `_hxNoBoost` instead.)_
@ -57,20 +60,20 @@ _(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true
Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these, there are modules with those values. For example, `HxTrigger.Load` could be used in the example above, to ensure that the known values are spelled correctly. `hx-trigger` can also take modifiers, such as an action that only responds to `Ctrl`+click. The `HxTrigger` module has a `Filter` submodule to assist with defining these actions. Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these, there are modules with those values. For example, `HxTrigger.Load` could be used in the example above, to ensure that the known values are spelled correctly. `hx-trigger` can also take modifiers, such as an action that only responds to `Ctrl`+click. The `HxTrigger` module has a `Filter` submodule to assist with defining these actions.
```fsharp ```fsharp
let shiftClick = let shiftClick =
p [ _hxGet = "/something"; _hxTrigger (HxTrigger.Filter.Shift HxTrigger.Click) ] [ p [ _hxGet = "/something"; _hxTrigger (HxTrigger.Filter.Shift HxTrigger.Click) ] [
str "hold down Shift and click me" str "hold down Shift and click me"
] ]
``` ```
If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees. If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees.
## Feedback / Help ## Feedback / Help
The author hangs out in the #htmx-general channel 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 of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/).
## Thanks ## Thanks
|[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)|[<img src="https://bitbadger.solutions/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)|
| :---: | :---: | :---: | | :---: |:------------------------------------------------------------------------------------------------------------------------------------:| :---: |
|for making ASP.NET Core functional|for making HTML cool again|for licensing their tools to this project| |for making ASP.NET Core functional| for making HTML cool again |for licensing their tools to this project|

28
src/Common/Common.fs Normal file
View File

@ -0,0 +1,28 @@
/// Common definitions shared between attribute values and response headers
[<AutoOpen>]
module Giraffe.Htmx.Common
/// Valid values for the `hx-swap` attribute / `HX-Reswap` header (may be combined with swap/settle/scroll/show config)
[<RequireQualifiedAccess>]
module HxSwap =
/// The default, replace the inner html of the target element
let InnerHtml = "innerHTML"
/// Replace the entire target element with the response
let OuterHtml = "outerHTML"
/// Insert the response before the target element
let BeforeBegin = "beforebegin"
/// Insert the response before the first child of the target element
let AfterBegin = "afterbegin"
/// Insert the response after the last child of the target element
let BeforeEnd = "beforeend"
/// Insert the response after the target element
let AfterEnd = "afterend"
/// Does not append content from response (out of band items will still be processed).
let None = "none"

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Common definitions for Giraffe.Htmx</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Common.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="FSharp.Core" Version="6.0.0" />
</ItemGroup>
</Project>

5
src/Common/README.md Normal file
View File

@ -0,0 +1,5 @@
## Giraffe.Htmx.Common
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one.
**htmx version: 2.0.2**

View File

@ -1,13 +1,14 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<VersionPrefix>1.6.1</VersionPrefix> <TargetFrameworks>net6.0;net8.0</TargetFrameworks>
<PackageReleaseNotes>Initial production-ready release</PackageReleaseNotes> <VersionPrefix>2.0.2</VersionPrefix>
<PackageReleaseNotes>Update script tags to pull htmx 2.0.2 (no header or attribute changes)</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl> <PackageProjectUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/bit-badger/Giraffe.Htmx</RepositoryUrl> <RepositoryUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright> <Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>

40
src/Giraffe.Htmx.sln Normal file
View File

@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx", "Htmx\Giraffe.Htmx.fsproj", "{8AB3085C-5236-485A-8565-A09106E72E1E}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.ViewEngine.Htmx", "ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj", "{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx.Common", "Common\Giraffe.Htmx.Common.fsproj", "{75D66845-F93A-4463-AD29-A8B16E4D4BA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{39823773-4311-4E79-9CA0-F9DDC40CAF6A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.Build.0 = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.Build.0 = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.Build.0 = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@ -1,34 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="NSubstitute" Version="4.2.2" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Htmx\Giraffe.Htmx.fsproj" />
</ItemGroup>
</Project>

View File

@ -1 +0,0 @@
module Program = let [<EntryPoint>] main _ = 0

View File

@ -1,329 +0,0 @@
module Giraffe.Htmx.Tests
open System
open Giraffe.Htmx
open Microsoft.AspNetCore.Http
open NSubstitute
open Xunit
/// Tests for the IHeaderDictionary extension properties
module IHeaderDictionaryExtensions =
[<Fact>]
let ``HxBoosted succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxBoosted |> Assert.True
[<Fact>]
let ``HxBoosted succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True
Option.get ctx.Request.Headers.HxBoosted |> Assert.True
[<Fact>]
let ``HxBoosted succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxBoosted |> Assert.True
Option.get ctx.Request.Headers.HxBoosted |> Assert.False
[<Fact>]
let ``HxCurrentUrl succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxCurrentUrl |> Assert.True
[<Fact>]
let ``HxCurrentUrl succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Current-URL", "http://localhost/test.htm")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxCurrentUrl |> Assert.True
Assert.Equal (Uri "http://localhost/test.htm", Option.get ctx.Request.Headers.HxCurrentUrl)
[<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
[<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
[<Fact>]
let ``HxHistoryRestoreRequest succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.True
Option.get ctx.Request.Headers.HxHistoryRestoreRequest |> Assert.False
[<Fact>]
let ``HxPrompt succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxPrompt |> Assert.True
[<Fact>]
let ``HxPrompt 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
Option.isSome ctx.Request.Headers.HxPrompt |> Assert.True
Assert.Equal("of course", Option.get ctx.Request.Headers.HxPrompt)
[<Fact>]
let ``HxRequest succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxRequest |> Assert.True
[<Fact>]
let ``HxRequest succeeds when the header is present and true`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxRequest |> Assert.True
Option.get ctx.Request.Headers.HxRequest |> Assert.True
[<Fact>]
let ``HxRequest succeeds when the header is present and false`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxRequest |> Assert.True
Option.get ctx.Request.Headers.HxRequest |> Assert.False
[<Fact>]
let ``HxTarget succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTarget |> Assert.True
[<Fact>]
let ``HxTarget succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Target", "#leItem")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTarget |> Assert.True
Assert.Equal("#leItem", Option.get ctx.Request.Headers.HxTarget)
[<Fact>]
let ``HxTrigger succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTrigger |> Assert.True
[<Fact>]
let ``HxTrigger succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTrigger |> Assert.True
Assert.Equal("#trig", Option.get ctx.Request.Headers.HxTrigger)
[<Fact>]
let ``HxTriggerName succeeds when the header is not present`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Option.isNone ctx.Request.Headers.HxTriggerName |> Assert.True
[<Fact>]
let ``HxTriggerName succeeds when the header is present`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Trigger-Name", "click")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Option.isSome ctx.Request.Headers.HxTriggerName |> Assert.True
Assert.Equal("click", Option.get ctx.Request.Headers.HxTriggerName)
/// Tests for the HttpRequest extension properties
module HttpRequestExtensions =
[<Fact>]
let ``IsHtmx succeeds when request is not from htmx`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Assert.False ctx.Request.IsHtmx
[<Fact>]
let ``IsHtmx succeeds when request is from htmx`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.True ctx.Request.IsHtmx
[<Fact>]
let ``IsHtmxRefresh succeeds when request is not from htmx`` () =
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Assert.False ctx.Request.IsHtmxRefresh
[<Fact>]
let ``IsHtmxRefresh succeeds when request is from htmx, but not a refresh`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.False ctx.Request.IsHtmxRefresh
[<Fact>]
let ``IsHtmxRefresh succeeds when request is from htmx and is a refresh`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Assert.True ctx.Request.IsHtmxRefresh
/// Tests for the HttpHandler functions provided in the Handlers module
module HandlerTests =
open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate
let next (ctx : HttpContext) = Task.FromResult (Some ctx)
[<Fact>]
let ``withHxPush succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxPush "/a-new-url" next ctx
Assert.True (dic.ContainsKey "HX-Push")
Assert.Equal ("/a-new-url", dic.["HX-Push"].[0])
}
[<Fact>]
let ``withHxRedirect succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxRedirect "/somewhere-else" next ctx
Assert.True (dic.ContainsKey "HX-Redirect")
Assert.Equal ("/somewhere-else", dic.["HX-Redirect"].[0])
}
[<Fact>]
let ``withHxRefresh succeeds when set to true`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxRefresh true next ctx
Assert.True (dic.ContainsKey "HX-Refresh")
Assert.Equal ("true", dic.["HX-Refresh"].[0])
}
[<Fact>]
let ``withHxRefresh succeeds when set to false`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxRefresh false next ctx
Assert.True (dic.ContainsKey "HX-Refresh")
Assert.Equal ("false", dic.["HX-Refresh"].[0])
}
[<Fact>]
let ``withHxRetarget succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxRetarget "#somewhereElse" next ctx
Assert.True (dic.ContainsKey "HX-Retarget")
Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0])
}
[<Fact>]
let ``withHxTrigger succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTrigger "doSomething" next ctx
Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("doSomething", dic.["HX-Trigger"].[0])
}
[<Fact>]
let ``withHxTriggerMany succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger")
Assert.Equal ("""{ "blah": "foo", "bleh": "bar" }""", dic.["HX-Trigger"].[0])
}
[<Fact>]
let ``withHxTriggerAfterSettle succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("byTheWay", dic.["HX-Trigger-After-Settle"].[0])
}
[<Fact>]
let ``withHxTriggerManyAfterSettle succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Settle")
Assert.Equal ("""{ "oof": "ouch", "hmm": "uh" }""", dic.["HX-Trigger-After-Settle"].[0])
}
[<Fact>]
let ``withHxTriggerAfterSwap succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerAfterSwap "justASec" next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
Assert.Equal ("justASec", dic.["HX-Trigger-After-Swap"].[0])
}
[<Fact>]
let ``withHxTriggerManyAfterSwap succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx
Assert.True (dic.ContainsKey "HX-Trigger-After-Swap")
Assert.Equal ("""{ "this": "1", "that": "2" }""", dic.["HX-Trigger-After-Swap"].[0])
}

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>htmx header extensions and helpers for Giraffe</Description> <Description>htmx header extensions and helpers for Giraffe</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
@ -10,10 +9,16 @@
<ItemGroup> <ItemGroup>
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" Version="6.4.0" />
<PackageReference Update="FSharp.Core" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,4 +1,4 @@
module Giraffe.Htmx module Giraffe.Htmx
open Microsoft.AspNetCore.Http open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Primitives open Microsoft.Extensions.Primitives
@ -6,98 +6,126 @@ open System
/// 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]
/// Extensions to the header dictionary /// Extensions to the header dictionary
type IHeaderDictionary with type IHeaderDictionary with
/// Indicates that the request is via an element using `hx-boost` /// Indicates that the request is via an element using `hx-boost`
member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse
/// The current URL of the browser _(note that this does not update until after settle)_ /// The current URL of the browser _(note that this does not update until after settle)_
member this.HxCurrentUrl with get () = hdr this "HX-Current-URL" |> Option.map Uri member this.HxCurrentUrl with get () = hdr this "HX-Current-URL" |> Option.map Uri
/// `true` if the request is for history restoration after a miss in the local history cache /// `true` if the request is for history restoration after a miss in the local history cache
member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
/// The user response to an `hx-prompt` /// The user response to an `hx-prompt`
member this.HxPrompt with get () = hdr this "HX-Prompt" member this.HxPrompt with get () = hdr this "HX-Prompt"
/// `true` if the request came from HTMX /// `true` if the request came from HTMX
member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse
/// The `id` of the target element if it exists /// The `id` of the target element if it exists
member this.HxTarget with get () = hdr this "HX-Target" member this.HxTarget with get () = hdr this "HX-Target"
/// The `id` of the triggered element if it exists /// The `id` of the triggered element if it exists
member this.HxTrigger with get () = hdr this "HX-Trigger" member this.HxTrigger with get () = hdr this "HX-Trigger"
/// The `name` of the triggered element if it exists /// The `name` of the triggered element if it exists
member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" member this.HxTriggerName with get () = hdr this "HX-Trigger-Name"
/// Extensions for the request object /// Extensions for the request object
type HttpRequest with type HttpRequest with
/// Whether this request was initiated from HTMX /// Whether this request was initiated from htmx
member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false
/// Whether this request is an HTMX history-miss refresh request /// Whether this request is an htmx history-miss refresh request
member this.IsHtmxRefresh with get () = member this.IsHtmxRefresh with get () =
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false) this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
/// HTTP handlers for setting output headers /// HTTP handlers for setting output headers
[<AutoOpen>] [<AutoOpen>]
module Handlers = module Handlers =
/// Convert a boolean to lowercase `true` or `false` /// Convert a boolean to lowercase `true` or `false`
let private toLowerBool (trueOrFalse : bool) = let private toLowerBool (trueOrFalse : bool) =
(string trueOrFalse).ToLowerInvariant () (string trueOrFalse).ToLowerInvariant ()
/// Serialize a list of key/value pairs to JSON (very rudimentary) /// Serialize a list of key/value pairs to JSON (very rudimentary)
let private toJson (evts : (string * string) list) = let private toJson (evts : (string * string) list) =
evts evts
|> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\""))) |> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\"")))
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
// Pushes a new url into the history stack /// Pushes a new url into the history stack
let withHxPush : string -> HttpHandler = let withHxPushUrl : string -> HttpHandler =
setHttpHeader "HX-Push" setHttpHeader "HX-Push-Url"
/// Can be used to do a client-side redirect to a new location /// Explicitly do not push a new URL into the history stack
let withHxRedirect : string -> HttpHandler = let withHxNoPushUrl : HttpHandler =
setHttpHeader "HX-Redirect" toLowerBool false |> withHxPushUrl
/// If set to `true` the client side will do a a full refresh of the page /// Pushes a new url into the history stack
let withHxRefresh : bool -> HttpHandler = [<Obsolete "Use withHxPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
toLowerBool >> setHttpHeader "HX-Refresh" let withHxPush = withHxPushUrl
/// Allows you to override the `hx-target` attribute /// Explicitly do not push a new URL into the history stack
let withHxRetarget : string -> HttpHandler = [<Obsolete "Use withHxNoPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">]
setHttpHeader "HX-Retarget" let withHxNoPush = withHxNoPushUrl
/// Allows you to trigger a single client side event /// Can be used to do a client-side redirect to a new location
let withHxTrigger : string -> HttpHandler = let withHxRedirect : string -> HttpHandler =
setHttpHeader "HX-Trigger" setHttpHeader "HX-Redirect"
/// Allows you to trigger multiple client side events /// If set to `true` the client side will do a a full refresh of the page
let withHxTriggerMany evts : HttpHandler = let withHxRefresh : bool -> HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger" toLowerBool >> setHttpHeader "HX-Refresh"
/// Allows you to trigger a single client side event after changes have settled /// Replaces the current URL in the history stack
let withHxTriggerAfterSettle : string -> HttpHandler = let withHxReplaceUrl : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Settle" setHttpHeader "HX-Replace-Url"
/// Allows you to trigger multiple client side events after changes have settled /// Explicitly do not replace the current URL in the history stack
let withHxTriggerManyAfterSettle evts : HttpHandler = let withHxNoReplaceUrl : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle" toLowerBool false |> withHxReplaceUrl
/// Allows you to trigger a single client side event after DOM swapping occurs /// Override which portion of the response will be swapped into the target document
let withHxTriggerAfterSwap : string -> HttpHandler = let withHxReselect : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Swap" setHttpHeader "HX-Reselect"
/// Allows you to trigger multiple client side events after DOM swapping occurs /// Override the `hx-swap` attribute from the initiating element
let withHxTriggerManyAfterSwap evts : HttpHandler = let withHxReswap : string -> HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap" setHttpHeader "HX-Reswap"
/// Allows you to override the `hx-target` attribute
let withHxRetarget : string -> HttpHandler =
setHttpHeader "HX-Retarget"
/// Allows you to trigger a single client side event
let withHxTrigger : string -> HttpHandler =
setHttpHeader "HX-Trigger"
/// Allows you to trigger multiple client side events
let withHxTriggerMany evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger"
/// Allows you to trigger a single client side event after changes have settled
let withHxTriggerAfterSettle : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Settle"
/// Allows you to trigger multiple client side events after changes have settled
let withHxTriggerManyAfterSettle evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
/// Allows you to trigger a single client side event after DOM swapping occurs
let withHxTriggerAfterSwap : string -> HttpHandler =
setHttpHeader "HX-Trigger-After-Swap"
/// Allows you to trigger multiple client side events after DOM swapping occurs
let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap"

View File

@ -2,7 +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: 1.6.1** **htmx version: 2.0.2**
_Upgrading from v1.x: the [migration guide](https://htmx.org/migration-guide-htmx-1/) does not currently specify any request or response header changes. This means that there are no required code changes in moving from v1.* to v2.*._
### Setup ### Setup
@ -14,22 +16,24 @@ This package enables server-side support for [htmx](https://htmx.org) within [Gi
To obtain a request header, using the `IHeaderDictionary` extension properties: To obtain a request header, using the `IHeaderDictionary` extension properties:
```fsharp ```fsharp
let myHandler : HttpHander = let myHandler : HttpHander =
fun next ctx -> fun next ctx ->
match ctx.HxPrompt with match ctx.HxPrompt with
| Some prompt -> ... // do something with the text the user provided | Some prompt -> ... // do something with the text the user provided
| None -> ... // no text provided | None -> ... // no text provided
``` ```
To set a response header: To set a response header:
```fsharp ```fsharp
let myHandler : HttpHander = let myHandler : HttpHander =
fun next ctx -> fun next ctx ->
// some meaningful work // some meaningful work
withHxPush "/some/new/url" >=> [other handlers] withHxPushUrl "/some/new/url" >=> [other handlers]
``` ```
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.
### Learn ### Learn
The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events. The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events.

33
src/Tests/Common.fs Normal file
View File

@ -0,0 +1,33 @@
module Common
open Expecto
open Giraffe.Htmx
/// Tests for the HxSwap module
let swap =
testList "HxSwap" [
test "InnerHtml is correct" {
Expect.equal HxSwap.InnerHtml "innerHTML" "Inner HTML swap value incorrect"
}
test "OuterHtml is correct" {
Expect.equal HxSwap.OuterHtml "outerHTML" "Outer HTML swap value incorrect"
}
test "BeforeBegin is correct" {
Expect.equal HxSwap.BeforeBegin "beforebegin" "Before Begin swap value incorrect"
}
test "BeforeEnd is correct" {
Expect.equal HxSwap.BeforeEnd "beforeend" "Before End swap value incorrect"
}
test "AfterBegin is correct" {
Expect.equal HxSwap.AfterBegin "afterbegin" "After Begin swap value incorrect"
}
test "AfterEnd is correct" {
Expect.equal HxSwap.AfterEnd "afterend" "After End swap value incorrect"
}
test "None is correct" {
Expect.equal HxSwap.None "none" "None swap value incorrect"
}
]
/// All tests for this module
let allTests = testList "Htmx.Common" [ swap ]

350
src/Tests/Htmx.fs Normal file
View File

@ -0,0 +1,350 @@
module Htmx
open System
open Expecto
open Giraffe.Htmx
open Microsoft.AspNetCore.Http
open NSubstitute
/// Tests for the IHeaderDictionary extension properties
let dictExtensions =
testList "IHeaderDictionaryExtensions" [
testList "HxBoosted" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned"
}
test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true"
}
test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Boosted", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false"
}
]
testList "HxCurrentUrl" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Current-URL", "http://localhost/test.htm")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present"
Expect.equal
ctx.Request.Headers.HxCurrentUrl.Value (Uri "http://localhost/test.htm")
"The header value was not correct"
}
]
testList "HxHistoryRestoreRequest" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned"
}
test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true"
}
test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-History-Restore-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
Expect.isFalse
ctx.Request.Headers.HxHistoryRestoreRequest.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" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned"
}
test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true"
}
test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false"
}
]
testList "HxTarget" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Target", "#leItem")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present"
Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect"
}
]
testList "HxTrigger" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present"
Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect"
}
]
testList "HxTriggerName" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned"
}
test "HxTriggerName succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Trigger-Name", "click")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present"
Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect"
}
]
]
/// Tests for the HttpRequest extension properties
let reqExtensions =
testList "HttpRequestExtensions" [
testList "IsHtmx" [
test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request"
}
test "succeeds when request is from htmx" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request"
}
]
testList "IsHtmxRefresh" [
test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext> ()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
}
test "succeeds when request is from htmx, but not a refresh" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
}
test "IsHtmxRefresh succeeds when request is from htmx and is a refresh" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Request", "true")
dic.Add ("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh"
}
]
]
open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate
let next (ctx : HttpContext) = Task.FromResult (Some ctx)
/// Tests for the HttpHandler functions provided in the Handlers module
let handlers =
testList "HandlerTests" [
testTask "withHxPushUrl succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxPushUrl "/a-new-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect"
}
testTask "withHxNoPushUrl succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoPushUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect"
}
testTask "withHxRedirect succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRedirect "/somewhere-else" next ctx
Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present"
Expect.equal dic["HX-Redirect"].[0] "/somewhere-else" "The HX-Redirect value was incorrect"
}
testList "withHxRefresh" [
testTask "succeeds when set to true" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh true next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect"
}
testTask "succeeds when set to false" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh false next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
Expect.equal dic["HX-Refresh"].[0] "false" "The HX-Refresh value was incorrect"
}
]
testTask "withHxReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect"
}
testTask "withHxNoReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoReplaceUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect"
}
testTask "withHxReselect succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReselect "#test" next ctx
Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present"
Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect"
}
testTask "withHxReswap succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present"
Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect"
}
testTask "withHxRetarget succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRetarget "#somewhereElse" next ctx
Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present"
Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect"
}
testTask "withHxTrigger succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTrigger "doSomething" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect"
}
testTask "withHxTriggerMany succeeds" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal
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"
}
]
/// All tests for this module
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers ]

6
src/Tests/Program.fs Normal file
View File

@ -0,0 +1,6 @@
open Expecto
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests ]
[<EntryPoint>]
let main args = runTestsWithCLIArgs [] args allTests

26
src/Tests/Tests.fsproj Normal file
View File

@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
</PropertyGroup>
<ItemGroup>
<Compile Include="Common.fs" />
<Compile Include="Htmx.fs" />
<Compile Include="ViewEngine.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
<ProjectReference Include="..\Htmx\Giraffe.Htmx.fsproj" />
<ProjectReference Include="..\ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Expecto" Version="10.2.1" />
<PackageReference Include="NSubstitute" Version="5.1.0" />
<PackageReference Update="FSharp.Core" Version="8.0.300" />
</ItemGroup>
</Project>

994
src/Tests/ViewEngine.fs Normal file
View File

@ -0,0 +1,994 @@
module ViewEngine
open Expecto
open Giraffe.ViewEngine
open Giraffe.ViewEngine.Htmx
/// Tests for the HxEncoding module
let hxEncoding =
testList "HxEncoding" [
test "Form is correct" {
Expect.equal HxEncoding.Form "application/x-www-form-urlencoded" "Form encoding not correct"
}
test "MultipartForm is correct" {
Expect.equal HxEncoding.MultipartForm "multipart/form-data" "Multipart form encoding not correct"
}
]
let hxEvent =
testList "HxEvent" [
testList "Abort" [
test "ToString succeeds" {
Expect.equal (string Abort) "abort" "Abort event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (Abort.ToHxOnString()) "abort" "Abort hx-on event name not correct"
}
]
testList "AfterOnLoad" [
test "ToString succeeds" {
Expect.equal (string AfterOnLoad) "afterOnLoad" "AfterOnLoad event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (AfterOnLoad.ToHxOnString()) "after-on-load" "AfterOnLoad hx-on event name not correct"
}
]
testList "AfterProcessNode" [
test "ToString succeeds" {
Expect.equal (string AfterProcessNode) "afterProcessNode" "AfterProcessNode event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterProcessNode.ToHxOnString())
"after-process-node"
"AfterProcessNode hx-on event name not correct"
}
]
testList "AfterRequest" [
test "ToString succeeds" {
Expect.equal (string AfterRequest) "afterRequest" "AfterRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (AfterRequest.ToHxOnString()) "after-request" "AfterRequest hx-on event name not correct"
}
]
testList "AfterSettle" [
test "ToString succeeds" {
Expect.equal (string AfterSettle) "afterSettle" "AfterSettle event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (AfterSettle.ToHxOnString()) "after-settle" "AfterSettle hx-on event name not correct"
}
]
testList "AfterSwap" [
test "ToString succeeds" {
Expect.equal (string AfterSwap) "afterSwap" "AfterSwap event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (AfterSwap.ToHxOnString()) "after-swap" "AfterSwap hx-on event name not correct"
}
]
testList "BeforeCleanupElement" [
test "ToString succeeds" {
Expect.equal
(string BeforeCleanupElement) "beforeCleanupElement" "BeforeCleanupElement event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeCleanupElement.ToHxOnString())
"before-cleanup-element"
"BeforeCleanupElement hx-on event name not correct"
}
]
testList "BeforeOnLoad" [
test "ToString succeeds" {
Expect.equal (string BeforeOnLoad) "beforeOnLoad" "BeforeOnLoad event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (BeforeOnLoad.ToHxOnString()) "before-on-load" "BeforeOnLoad hx-on event name not correct"
}
]
testList "BeforeProcessNode" [
test "ToString succeeds" {
Expect.equal (string BeforeProcessNode) "beforeProcessNode" "BeforeProcessNode event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeProcessNode.ToHxOnString())
"before-process-node"
"BeforeProcessNode hx-on event name not correct"
}
]
testList "BeforeRequest" [
test "ToString succeeds" {
Expect.equal (string BeforeRequest) "beforeRequest" "BeforeRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeRequest.ToHxOnString()) "before-request" "BeforeRequest hx-on event name not correct"
}
]
testList "BeforeSwap" [
test "ToString succeeds" {
Expect.equal (string BeforeSwap) "beforeSwap" "BeforeSwap event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (BeforeSwap.ToHxOnString()) "before-swap" "BeforeSwap hx-on event name not correct"
}
]
testList "BeforeSend" [
test "ToString succeeds" {
Expect.equal (string BeforeSend) "beforeSend" "BeforeSend event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (BeforeSend.ToHxOnString()) "before-send" "BeforeSend hx-on event name not correct"
}
]
testList "ConfigRequest" [
test "ToString succeeds" {
Expect.equal (string ConfigRequest) "configRequest" "ConfigRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(ConfigRequest.ToHxOnString()) "config-request" "ConfigRequest hx-on event name not correct"
}
]
testList "Confirm" [
test "ToString succeeds" {
Expect.equal (string Confirm) "confirm" "Confirm event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (Confirm.ToHxOnString()) "confirm" "Confirm hx-on event name not correct"
}
]
testList "HistoryCacheError" [
test "ToString succeeds" {
Expect.equal (string HistoryCacheError) "historyCacheError" "HistoryCacheError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(HistoryCacheError.ToHxOnString())
"history-cache-error"
"HistoryCacheError hx-on event name not correct"
}
]
testList "HistoryCacheMiss" [
test "ToString succeeds" {
Expect.equal (string HistoryCacheMiss) "historyCacheMiss" "HistoryCacheMiss event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(HistoryCacheMiss.ToHxOnString())
"history-cache-miss"
"HistoryCacheMiss hx-on event name not correct"
}
]
testList "HistoryCacheMissError" [
test "ToString succeeds" {
Expect.equal
(string HistoryCacheMissError)
"historyCacheMissError"
"HistoryCacheMissError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(HistoryCacheMissError.ToHxOnString())
"history-cache-miss-error"
"HistoryCacheMissError hx-on event name not correct"
}
]
testList "HistoryCacheMissLoad" [
test "ToString succeeds" {
Expect.equal
(string HistoryCacheMissLoad) "historyCacheMissLoad" "HistoryCacheMissLoad event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(HistoryCacheMissLoad.ToHxOnString())
"history-cache-miss-load"
"HistoryCacheMissLoad hx-on event name not correct"
}
]
testList "HistoryRestore" [
test "ToString succeeds" {
Expect.equal (string HistoryRestore) "historyRestore" "HistoryRestore event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(HistoryRestore.ToHxOnString()) "history-restore" "HistoryRestore hx-on event name not correct"
}
]
testList "BeforeHistorySave" [
test "ToString succeeds" {
Expect.equal (string BeforeHistorySave) "beforeHistorySave" "BeforeHistorySave event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeHistorySave.ToHxOnString())
"before-history-save"
"BeforeHistorySave hx-on event name not correct"
}
]
testList "Load" [
test "ToString succeeds" {
Expect.equal (string Load) "load" "Load event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (Load.ToHxOnString()) "load" "Load hx-on event name not correct"
}
]
testList "NoSseSourceError" [
test "ToString succeeds" {
Expect.equal (string NoSseSourceError) "noSSESourceError" "NoSseSourceError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(NoSseSourceError.ToHxOnString())
"no-sse-source-error"
"NoSseSourceError hx-on event name not correct"
}
]
testList "OnLoadError" [
test "ToString succeeds" {
Expect.equal (string OnLoadError) "onLoadError" "OnLoadError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (OnLoadError.ToHxOnString()) "on-load-error" "OnLoadError hx-on event name not correct"
}
]
testList "OobAfterSwap" [
test "ToString succeeds" {
Expect.equal (string OobAfterSwap) "oobAfterSwap" "OobAfterSwap event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (OobAfterSwap.ToHxOnString()) "oob-after-swap" "OobAfterSwap hx-on event name not correct"
}
]
testList "OobBeforeSwap" [
test "ToString succeeds" {
Expect.equal (string OobBeforeSwap) "oobBeforeSwap" "OobBeforeSwap event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(OobBeforeSwap.ToHxOnString()) "oob-before-swap" "OobBeforeSwap hx-on event name not correct"
}
]
testList "OobErrorNoTarget" [
test "ToString succeeds" {
Expect.equal (string OobErrorNoTarget) "oobErrorNoTarget" "OobErrorNoTarget event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(OobErrorNoTarget.ToHxOnString())
"oob-error-no-target"
"OobErrorNoTarget hx-on event name not correct"
}
]
testList "Prompt" [
test "ToString succeeds" {
Expect.equal (string Prompt) "prompt" "Prompt event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (Prompt.ToHxOnString()) "prompt" "Prompt hx-on event name not correct"
}
]
testList "PushedIntoHistory" [
test "ToString succeeds" {
Expect.equal (string PushedIntoHistory) "pushedIntoHistory" "PushedIntoHistory event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(PushedIntoHistory.ToHxOnString())
"pushed-into-history"
"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()) "response-error" "ResponseError hx-on event name not correct"
}
]
testList "SendError" [
test "ToString succeeds" {
Expect.equal (string SendError) "sendError" "SendError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SendError.ToHxOnString()) "send-error" "SendError 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"
}
]
testList "SseOpen" [
test "ToString succeeds" {
Expect.equal (string SseOpen) "sseOpen" "SseOpen event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SseOpen.ToHxOnString()) "sse-open" "SseOpen hx-on event name not correct"
}
]
testList "SwapError" [
test "ToString succeeds" {
Expect.equal (string SwapError) "swapError" "SwapError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SwapError.ToHxOnString()) "swap-error" "SwapError hx-on event name not correct"
}
]
testList "TargetError" [
test "ToString succeeds" {
Expect.equal (string TargetError) "targetError" "TargetError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (TargetError.ToHxOnString()) "target-error" "TargetError hx-on event name not correct"
}
]
testList "Timeout" [
test "ToString succeeds" {
Expect.equal (string Timeout) "timeout" "Timeout event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (Timeout.ToHxOnString()) "timeout" "Timeout hx-on event name not correct"
}
]
testList "ValidationValidate" [
test "ToString succeeds" {
Expect.equal
(string ValidationValidate) "validation:validate" "ValidationValidate event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(ValidationValidate.ToHxOnString())
"validation:validate"
"ValidationValidate hx-on event name not correct"
}
]
testList "ValidationFailed" [
test "ToString succeeds" {
Expect.equal (string ValidationFailed) "validation:failed" "ValidationFailed event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(ValidationFailed.ToHxOnString())
"validation:failed"
"ValidationFailed hx-on event name not correct"
}
]
testList "ValidationHalted" [
test "ToString succeeds" {
Expect.equal (string ValidationHalted) "validation:halted" "ValidationHalted event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(ValidationHalted.ToHxOnString())
"validation:halted"
"ValidationHalted hx-on event name not correct"
}
]
testList "XhrAbort" [
test "ToString succeeds" {
Expect.equal (string XhrAbort) "xhr:abort" "XhrAbort event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (XhrAbort.ToHxOnString()) "xhr:abort" "XhrAbort hx-on event name not correct"
}
]
testList "XhrLoadEnd" [
test "ToString succeeds" {
Expect.equal (string XhrLoadEnd) "xhr:loadend" "XhrLoadEnd event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (XhrLoadEnd.ToHxOnString()) "xhr:loadend" "XhrLoadEnd hx-on event name not correct"
}
]
testList "XhrLoadStart" [
test "ToString succeeds" {
Expect.equal (string XhrLoadStart) "xhr:loadstart" "XhrLoadStart event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (XhrLoadStart.ToHxOnString()) "xhr:loadstart" "XhrLoadStart hx-on event name not correct"
}
]
testList "XhrProgress" [
test "ToString succeeds" {
Expect.equal (string XhrProgress) "xhr:progress" "XhrProgress event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct"
}
]
]
/// Tests for the HxHeaders module
let hxHeaders =
testList "HxHeaders" [
testList "From" [
test "succeeds with an empty list" {
Expect.equal (HxHeaders.From []) "{ }" "Empty headers not correct"
}
test "succeeds and escapes quotes" {
Expect.equal
(HxHeaders.From [ "test", "one two three"; "again", """four "five" six""" ])
"""{ "test": "one two three", "again": "four \"five\" six" }""" "Headers not correct"
}
]
]
/// Tests for the HxParams module
let hxParams =
testList "HxParams" [
test "All is correct" {
Expect.equal HxParams.All "*" "All is not correct"
}
test "None is correct" {
Expect.equal HxParams.None "none" "None is not correct"
}
testList "With" [
test "succeeds with empty list" {
Expect.equal (HxParams.With []) "" "With with empty list should have been blank"
}
test "succeeds with one list item" {
Expect.equal (HxParams.With [ "boo" ]) "boo" "With single item incorrect"
}
test "succeeds with multiple list items" {
Expect.equal (HxParams.With [ "foo"; "bar"; "baz" ]) "foo,bar,baz" "With multiple items incorrect"
}
]
testList "Except" [
test "succeeds with empty list" {
Expect.equal (HxParams.Except []) "not " "Except with empty list incorrect"
}
test "succeeds with one list item" {
Expect.equal (HxParams.Except [ "that" ]) "not that" "Except single item incorrect"
}
test "succeeds with multiple list items" {
Expect.equal (HxParams.Except [ "blue"; "green" ]) "not blue,green" "Except multiple items incorrect"
}
]
]
/// Tests for the HxRequest module
let hxRequest =
testList "HxRequest" [
testList "Configure" [
test "succeeds with an empty list" {
Expect.equal (HxRequest.Configure []) "{ }" "Configure with empty list incorrect"
}
test "succeeds with a non-empty list" {
Expect.equal
(HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) """{ "a": "b", "c": "d" }"""
"Configure with a non-empty list incorrect"
}
test "succeeds with all known params configured" {
Expect.equal
(HxRequest.Configure
[ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
"""{ "timeout": 1000, "credentials": false, "noHeaders": true }"""
"Configure with all known params incorrect"
}
]
test "Timeout succeeds" {
Expect.equal (HxRequest.Timeout 50) "\"timeout\": 50" "Timeout value incorrect"
}
testList "Credentials" [
test "succeeds when set to true" {
Expect.equal (HxRequest.Credentials true) "\"credentials\": true" "Credentials value incorrect"
}
test "succeeds when set to false" {
Expect.equal (HxRequest.Credentials false) "\"credentials\": false" "Credentials value incorrect"
}
]
testList "NoHeaders" [
test "succeeds when set to true" {
Expect.equal (HxRequest.NoHeaders true) "\"noHeaders\": true" "NoHeaders value incorrect"
}
test "succeeds when set to false" {
Expect.equal (HxRequest.NoHeaders false) "\"noHeaders\": false" "NoHeaders value incorrect"
}
]
]
/// Tests for the HxTrigger module
let hxTrigger =
testList "HxTrigger" [
test "Click is correct" {
Expect.equal HxTrigger.Click "click" "Click is incorrect"
}
test "Load is correct" {
Expect.equal HxTrigger.Load "load" "Load is incorrect"
}
test "Revealed is correct" {
Expect.equal HxTrigger.Revealed "revealed" "Revealed is incorrect"
}
test "Every succeeds" {
Expect.equal (HxTrigger.Every "3s") "every 3s" "Every is incorrect"
}
testList "Filter" [
test "Alt succeeds" {
Expect.equal (HxTrigger.Filter.Alt HxTrigger.Click) "click[altKey]" "Alt filter incorrect"
}
test "Ctrl succeeds" {
Expect.equal (HxTrigger.Filter.Ctrl HxTrigger.Click) "click[ctrlKey]" "Ctrl filter incorrect"
}
test "Shift succeeds" {
Expect.equal (HxTrigger.Filter.Shift HxTrigger.Click) "click[shiftKey]" "Shift filter incorrect"
}
test "CtrlAlt succeeds" {
Expect.equal
(HxTrigger.Filter.CtrlAlt HxTrigger.Click) "click[ctrlKey&&altKey]" "Ctrl+Alt filter incorrect"
}
test "CtrlShift succeeds" {
Expect.equal
(HxTrigger.Filter.CtrlShift HxTrigger.Click) "click[ctrlKey&&shiftKey]"
"Ctrl+Shift filter incorrect"
}
test "CtrlAltShift succeeds" {
Expect.equal
(HxTrigger.Filter.CtrlAltShift HxTrigger.Click) "click[ctrlKey&&altKey&&shiftKey]"
"Ctrl+Alt+Shift filter incorrect"
}
test "AltShift succeeds" {
Expect.equal
(HxTrigger.Filter.AltShift HxTrigger.Click) "click[altKey&&shiftKey]" "Alt+Shift filter incorrect"
}
]
testList "Once" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Once "") "once" "Once modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Once "click") "click once" "Once modifier incorrect"
}
]
testList "Changed" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Changed "") "changed" "Changed modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Changed "click") "click changed" "Changed modifier incorrect"
}
]
testList "Delay" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Delay "1s" "") "delay:1s" "Delay modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Delay "2s" "click") "click delay:2s" "Delay modifier incorrect"
}
]
testList "Throttle" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Throttle "4s" "") "throttle:4s" "Throttle modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Throttle "7s" "click") "click throttle:7s" "Throttle modifier incorrect"
}
]
testList "From" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.From ".nav" "") "from:.nav" "From modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.From "#somewhere" "click") "click from:#somewhere" "From modifier incorrect"
}
]
testList "FromDocument" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.FromDocument "") "from:document" "FromDocument modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.FromDocument "click") "click from:document" "FromDocument modifier incorrect"
}
]
testList "FromWindow" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.FromWindow "") "from:window" "FromWindow modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.FromWindow "click") "click from:window" "FromWindow modifier incorrect"
}
]
testList "FromClosest" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.FromClosest "div" "") "from:closest div" "FromClosest modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.FromClosest "p" "click") "click from:closest p" "FromClosest modifier incorrect"
}
]
testList "FromFind" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.FromFind "li" "") "from:find li" "FromFind modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.FromFind ".spot" "click") "click from:find .spot" "FromFind modifier incorrect"
}
]
testList "Target" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Target "main" "") "target:main" "Target modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Target "footer" "click") "click target:footer" "Target modifier incorrect"
}
]
testList "Consume" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Consume "") "consume" "Consume modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Consume "click") "click consume" "Consume modifier incorrect"
}
]
testList "Queue" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.Queue "abc" "") "queue:abc" "Queue modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect"
}
]
testList "QueueFirst" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.QueueFirst "") "queue:first" "QueueFirst modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.QueueFirst "click") "click queue:first" "QueueFirst modifier incorrect"
}
]
testList "QueueLast" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.QueueLast "") "queue:last" "QueueLast modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.QueueLast "click") "click queue:last" "QueueLast modifier incorrect"
}
]
testList "QueueAll" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.QueueAll "") "queue:all" "QueueAll modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.QueueAll "click") "click queue:all" "QueueAll modifier incorrect"
}
]
testList "QueueNone" [
test "succeeds when it is the first modifier" {
Expect.equal (HxTrigger.QueueNone "") "queue:none" "QueueNone modifier incorrect"
}
test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.QueueNone "click") "click queue:none" "QueueNone modifier incorrect"
}
]
]
/// Tests for the HxVals module
let hxVals =
testList "HxVals" [
testList "From" [
test "succeeds with an empty list" {
Expect.equal (HxVals.From []) "{ }" "From with an empty list is incorrect"
}
test "succeeds and escapes quotes" {
Expect.equal
(HxVals.From [ "test", """a "b" c"""; "2", "d e f" ])
"""{ "test": "a \"b\" c", "2": "d e f" }""" "From value is incorrect"
}
]
]
/// Pipe-able assertion for a rendered node
let shouldRender expected node =
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
/// Tests for the HtmxAttrs module
let attributes =
testList "Attributes" [
test "_hxBoost succeeds" {
div [ _hxBoost ] [] |> shouldRender """<div hx-boost="true"></div>"""
}
test "_hxConfirm succeeds" {
button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """<button hx-confirm="REALLY?!?"></button>"""
}
test "_hxDelete succeeds" {
span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """<span hx-delete="/this-endpoint"></span>"""
}
test "_hxDisable succeeds" {
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
}
test "_hxDisabledElt succeeds" {
button [ _hxDisabledElt "this" ] [] |> shouldRender """<button hx-disabled-elt="this"></button>"""
}
test "_hxDisinherit succeeds" {
strong [ _hxDisinherit "*" ] [] |> shouldRender """<strong hx-disinherit="*"></strong>"""
}
test "_hxEncoding succeeds" {
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
}
test "_hxExt succeeds" {
section [ _hxExt "extendme" ] [] |> shouldRender """<section hx-ext="extendme"></section>"""
}
test "_hxGet succeeds" {
article [ _hxGet "/the-text" ] [] |> shouldRender """<article hx-get="/the-text"></article>"""
}
test "_hxHeaders succeeds" {
figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] []
|> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>"""
}
test "_hxHistory succeeds" {
span [ _hxHistory "false" ] [] |> shouldRender """<span hx-history="false"></span>"""
}
test "_hxHistoryElt succeeds" {
table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>"""
}
test "_hxInclude succeeds" {
a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """<a hx-include=".extra-stuff"></a>"""
}
test "_hxIndicator succeeds" {
aside [ _hxIndicator "#spinner" ] [] |> shouldRender """<aside hx-indicator="#spinner"></aside>"""
}
test "_hxNoBoost succeeds" {
td [ _hxNoBoost ] [] |> shouldRender """<td hx-boost="false"></td>"""
}
test "_hxOnEvent succeeds" {
a [ _hxOnEvent "click" "doThis()" ] [] |> shouldRender """<a hx-on:click="doThis()"></a>"""
}
test "_hxOnHxEvent succeeds" {
strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] []
|> shouldRender """<strong hx-on::before-swap="changeStuff()"></strong>"""
}
test "_hxParams succeeds" {
br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">"""
}
test "_hxPatch succeeds" {
div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>"""
}
test "_hxPost succeeds" {
hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">"""
}
test "_hxPreserve succeeds" {
img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">"""
}
test "_hxPrompt succeeds" {
strong [ _hxPrompt "Who goes there?" ] []
|> shouldRender """<strong hx-prompt="Who goes there?"></strong>"""
}
test "_hxPushUrl succeeds" {
dl [ _hxPushUrl "/a-b-c" ] [] |> shouldRender """<dl hx-push-url="/a-b-c"></dl>"""
}
test "_hxPut succeeds" {
s [ _hxPut "/take-this" ] [] |> shouldRender """<s hx-put="/take-this"></s>"""
}
test "_hxReplaceUrl succeeds" {
p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """<p hx-replace-url="/something-else"></p>"""
}
test "_hxRequest succeeds" {
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
}
test "_hxSelect succeeds" {
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
}
test "_hxSelectOob succeeds" {
section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
}
test "_hxSwap succeeds" {
del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>"""
}
test "_hxSwapWithTransition succeeds" {
del [ _hxSwapWithTransition "innerHTML" ] []
|> shouldRender """<del hx-swap="innerHTML transition:true"></del>"""
}
test "_hxSwapOob succeeds" {
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
}
test "_hxSync succeeds" {
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
}
test "_hxTarget succeeds" {
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
}
test "_hxTrigger succeeds" {
figcaption [ _hxTrigger "load" ] [] |> shouldRender """<figcaption hx-trigger="load"></figcaption>"""
}
test "_hxVals succeeds" {
dt [ _hxVals """{ "extra": "values" }""" ] []
|> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>"""
}
test "_sseSwap succeeds" {
ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """<ul sse-swap="sseMessageName"></ul>"""
}
test "_sseConnect succeeds" {
div [ _sseConnect "/gps/sse" ] [] |> shouldRender """<div sse-connect="/gps/sse"></div>"""
}
]
/// Tests for the Script module
let script =
testList "Script" [
test "minified succeeds" {
let html = RenderView.AsString.htmlNode Script.minified
Expect.equal
html
"""<script src="https://unpkg.com/htmx.org@2.0.2" integrity="sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ" crossorigin="anonymous"></script>"""
"Minified script tag is incorrect"
}
test "unminified succeeds" {
let html = RenderView.AsString.htmlNode Script.unminified
Expect.equal
html
"""<script src="https://unpkg.com/htmx.org@2.0.2/dist/htmx.js" integrity="sha384-yZq+5izaUBKcRgFbxgkRYwpHhHHCpp5nseXp0MEQ1A4MTWVMnqkmcuFez8x5qfxr" crossorigin="anonymous"></script>"""
"Unminified script tag is incorrect"
}
]
open System.Text
/// Tests for the RenderFragment module
let renderFragment =
testList "RenderFragment" [
/// Validate that the two object references are the same object
let isSame obj1 obj2 message =
Expect.isTrue (obj.ReferenceEquals (obj1, obj2)) message
testList "findIdNode" [
test "fails with a Text node" {
Expect.isNone (RenderFragment.findIdNode "blue" (Text "")) "There should not have been a node found"
}
test "fails with a VoidElement without a matching ID" {
Expect.isNone
(RenderFragment.findIdNode "purple" (br [ _id "mauve" ])) "There should not have been a node found"
}
test "fails with a ParentNode with no children with a matching ID" {
Expect.isNone
(RenderFragment.findIdNode "green" (p [] [ str "howdy"; span [] [ str "huh" ] ]))
"There should not have been a node found"
}
test "succeeds with a VoidElement with a matching ID" {
let leNode = hr [ _id "groovy" ]
let foundNode = RenderFragment.findIdNode "groovy" leNode
Expect.isSome foundNode "There should have been a node found"
isSame leNode foundNode.Value "The node should have been the same object"
}
test "succeeds with a ParentNode with a child with a matching ID" {
let leNode = span [ _id "its-me" ] [ str "Mario" ]
let foundNode =
RenderFragment.findIdNode "its-me" (p [] [ str "test"; str "again"; leNode; str "un mas" ])
Expect.isSome foundNode "There should have been a node found"
isSame leNode foundNode.Value "The node should have been the same object"
}
]
/// Generate a message if the requested ID node is not found
let nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
testList "AsString" [
testList "htmlFromNodes" [
test "succeeds when an ID is matched" {
let html =
RenderFragment.AsString.htmlFromNodes "needle"
[ p [] []
p [ _id "haystack" ] [ str "hay"; span [ _id "needle" ] [ str "ouch" ]; str "hay" ]
]
Expect.equal html """<span id="needle">ouch</span>""" "HTML is incorrect"
}
test "fails when an ID is not matched" {
Expect.equal
(RenderFragment.AsString.htmlFromNodes "oops" []) (nodeNotFound "oops") "HTML is incorrect"
}
]
testList "htmlFromNode" [
test "succeeds when ID is matched at top level" {
let html = RenderFragment.AsString.htmlFromNode "wow" (p [ _id "wow" ] [ str "found it" ])
Expect.equal html """<p id="wow">found it</p>""" "HTML is incorrect"
}
test "succeeds when ID is matched in child element" {
let html =
div [] [ p [] [ str "not it" ]; p [ _id "hey" ] [ str "ta-da" ]]
|> RenderFragment.AsString.htmlFromNode "hey"
Expect.equal html """<p id="hey">ta-da</p>""" "HTML is incorrect"
}
test "fails when an ID is not matched" {
Expect.equal
(RenderFragment.AsString.htmlFromNode "me" (hr [])) (nodeNotFound "me") "HTML is incorrect"
}
]
]
testList "AsBytes" [
/// Alias for UTF-8 encoding
let utf8 = Encoding.UTF8
testList "htmlFromNodes" [
test "succeeds when an ID is matched" {
let bytes =
RenderFragment.AsBytes.htmlFromNodes "found"
[ p [] []
p [ _id "not-it" ] [ str "nope"; span [ _id "found" ] [ str "boo" ]; str "nope" ]
]
Expect.equal bytes (utf8.GetBytes """<span id="found">boo</span>""") "HTML bytes are incorrect"
}
test "fails when an ID is not matched" {
Expect.equal
(RenderFragment.AsBytes.htmlFromNodes "whiff" []) (utf8.GetBytes (nodeNotFound "whiff"))
"HTML bytes are incorrect"
}
]
testList "htmlFromNode" [
test "succeeds when ID is matched at top level" {
let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ])
Expect.equal bytes (utf8.GetBytes """<p id="first">!!!</p>""") "HTML bytes are incorrect"
}
test "succeeds when ID is matched in child element" {
let bytes =
div [] [ p [] [ str "not me" ]; p [ _id "child" ] [ str "node" ]]
|> RenderFragment.AsBytes.htmlFromNode "child"
Expect.equal bytes (utf8.GetBytes """<p id="child">node</p>""") "HTML bytes are incorrect"
}
test "fails when an ID is not matched" {
Expect.equal
(RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) (utf8.GetBytes (nodeNotFound "foo"))
"HTML bytes are incorrect"
}
]
]
testList "IntoStringBuilder" [
testList "htmlFromNodes" [
test "succeeds when an ID is matched" {
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me"
[ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]]
Expect.equal (string sb) """<span id="find-me">;)</span>""" "HTML is incorrect"
}
test "fails when an ID is not matched" {
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" []
Expect.equal (string sb) (nodeNotFound "missing") "HTML is incorrect"
}
]
testList "htmlFromNode" [
test "succeeds when ID is matched at top level" {
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ])
Expect.equal (string sb) """<p id="top">pinnacle</p>""" "HTML is incorrect"
}
test "succeeds when ID is matched in child element" {
let sb = StringBuilder ()
div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]]
|> RenderFragment.IntoStringBuilder.htmlFromNode sb "it"
Expect.equal (string sb) """<p id="it">is here</p>""" "HTML is incorrect"
}
test "fails when an ID is not matched" {
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr [])
Expect.equal (string sb) (nodeNotFound "bar") "HTML is incorrect"
}
]
]
]
/// All tests in this module
let allTests =
testList "ViewEngine.Htmx" [
hxEncoding
hxEvent
hxHeaders
hxParams
hxRequest
hxTrigger
hxVals
attributes
script
renderFragment
]

View File

@ -1,33 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<IsPackable>false</IsPackable>
<GenerateProgramFile>false</GenerateProgramFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Tests.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="3.1.0">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj" />
</ItemGroup>
</Project>

View File

@ -1 +0,0 @@
module Program = let [<EntryPoint>] main _ = 0

View File

@ -1,444 +0,0 @@
module Giraffe.ViewEngine.Htmx.Tests
open Giraffe.ViewEngine
open Xunit
/// Tests for the HxEncoding module
module Encoding =
[<Fact>]
let ``Form is correct`` () =
Assert.Equal ("application/x-www-form-urlencoded", HxEncoding.Form)
[<Fact>]
let ``MultipartForm is correct`` () =
Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm)
/// Tests for the HxHeaders module
module Headers =
[<Fact>]
let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxHeaders.From [])
[<Fact>]
let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }",
HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ])
/// Tests for the HxParams module
module Params =
[<Fact>]
let ``All is correct`` () =
Assert.Equal ("*", HxParams.All)
[<Fact>]
let ``None is correct`` () =
Assert.Equal ("none", HxParams.None)
[<Fact>]
let ``With succeeds with empty list`` () =
Assert.Equal ("", HxParams.With [])
[<Fact>]
let ``With succeeds with one list item`` () =
Assert.Equal ("boo", HxParams.With [ "boo" ])
[<Fact>]
let ``With succeeds with multiple list items`` () =
Assert.Equal ("foo,bar,baz", HxParams.With [ "foo"; "bar"; "baz" ])
[<Fact>]
let ``Except succeeds with empty list`` () =
Assert.Equal ("not ", HxParams.Except [])
[<Fact>]
let ``Except succeeds with one list item`` () =
Assert.Equal ("not that", HxParams.Except [ "that" ])
[<Fact>]
let ``Except succeeds with multiple list items`` () =
Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ])
/// Tests for the HxRequest module
module Request =
[<Fact>]
let ``Configure succeeds with an empty list`` () =
Assert.Equal ("{ }", HxRequest.Configure [])
[<Fact>]
let ``Configure succeeds with a non-empty list`` () =
Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ])
[<Fact>]
let ``Configure succeeds with all known params configured`` () =
Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }",
HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
[<Fact>]
let ``Timeout succeeds`` () =
Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50)
[<Fact>]
let ``Credentials succeeds when set to true`` () =
Assert.Equal ("\"credentials\": true", HxRequest.Credentials true)
[<Fact>]
let ``Credentials succeeds when set to false`` () =
Assert.Equal ("\"credentials\": false", HxRequest.Credentials false)
[<Fact>]
let ``NoHeaders succeeds when set to true`` () =
Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true)
[<Fact>]
let ``NoHeaders succeeds when set to false`` () =
Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false)
/// Tests for the HxSwap module
module Swap =
[<Fact>]
let ``InnerHtml is correct`` () =
Assert.Equal ("innerHTML", HxSwap.InnerHtml)
[<Fact>]
let ``OuterHtml is correct`` () =
Assert.Equal ("outerHTML", HxSwap.OuterHtml)
[<Fact>]
let ``BeforeBegin is correct`` () =
Assert.Equal ("beforebegin", HxSwap.BeforeBegin)
[<Fact>]
let ``BeforeEnd is correct`` () =
Assert.Equal ("beforeend", HxSwap.BeforeEnd)
[<Fact>]
let ``AfterBegin is correct`` () =
Assert.Equal ("afterbegin", HxSwap.AfterBegin)
[<Fact>]
let ``AfterEnd is correct`` () =
Assert.Equal ("afterend", HxSwap.AfterEnd)
[<Fact>]
let ``None is correct`` () =
Assert.Equal ("none", HxSwap.None)
/// Tests for the HxTrigger module
module Trigger =
[<Fact>]
let ``Click is correct`` () =
Assert.Equal ("click", HxTrigger.Click)
[<Fact>]
let ``Load is correct`` () =
Assert.Equal ("load", HxTrigger.Load)
[<Fact>]
let ``Revealed is correct`` () =
Assert.Equal ("revealed", HxTrigger.Revealed)
[<Fact>]
let ``Every succeeds`` () =
Assert.Equal ("every 3s", HxTrigger.Every "3s")
[<Fact>]
let ``Filter.Alt succeeds`` () =
Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click)
[<Fact>]
let ``Filter.Ctrl succeeds`` () =
Assert.Equal ("click[ctrlKey]", HxTrigger.Filter.Ctrl HxTrigger.Click)
[<Fact>]
let ``Filter.Shift succeeds`` () =
Assert.Equal ("click[shiftKey]", HxTrigger.Filter.Shift HxTrigger.Click)
[<Fact>]
let ``Filter.CtrlAlt succeeds`` () =
Assert.Equal ("click[ctrlKey&&altKey]", HxTrigger.Filter.CtrlAlt HxTrigger.Click)
[<Fact>]
let ``Filter.CtrlShift succeeds`` () =
Assert.Equal ("click[ctrlKey&&shiftKey]", HxTrigger.Filter.CtrlShift HxTrigger.Click)
[<Fact>]
let ``Filter.CtrlAltShift succeeds`` () =
Assert.Equal ("click[ctrlKey&&altKey&&shiftKey]", HxTrigger.Filter.CtrlAltShift HxTrigger.Click)
[<Fact>]
let ``Filter.AltShift succeeds`` () =
Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click)
[<Fact>]
let ``Once succeeds when it is the first modifier`` () =
Assert.Equal ("once", HxTrigger.Once "")
[<Fact>]
let ``Once succeeds when it is not the first modifier`` () =
Assert.Equal ("click once", HxTrigger.Once "click")
[<Fact>]
let ``Changed succeeds when it is the first modifier`` () =
Assert.Equal ("changed", HxTrigger.Changed "")
[<Fact>]
let ``Changed succeeds when it is not the first modifier`` () =
Assert.Equal ("click changed", HxTrigger.Changed "click")
[<Fact>]
let ``Delay succeeds when it is the first modifier`` () =
Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "")
[<Fact>]
let ``Delay succeeds when it is not the first modifier`` () =
Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click")
[<Fact>]
let ``Throttle succeeds when it is the first modifier`` () =
Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "")
[<Fact>]
let ``Throttle succeeds when it is not the first modifier`` () =
Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click")
[<Fact>]
let ``From succeeds when it is the first modifier`` () =
Assert.Equal ("from:.nav", HxTrigger.From ".nav" "")
[<Fact>]
let ``From succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click")
[<Fact>]
let ``FromDocument succeeds when it is the first modifier`` () =
Assert.Equal ("from:document", HxTrigger.FromDocument "")
[<Fact>]
let ``FromDocument succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:document", HxTrigger.FromDocument "click")
[<Fact>]
let ``FromWindow succeeds when it is the first modifier`` () =
Assert.Equal ("from:window", HxTrigger.FromWindow "")
[<Fact>]
let ``FromWindow succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:window", HxTrigger.FromWindow "click")
[<Fact>]
let ``FromClosest succeeds when it is the first modifier`` () =
Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "")
[<Fact>]
let ``FromClosest succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click")
[<Fact>]
let ``FromFind succeeds when it is the first modifier`` () =
Assert.Equal ("from:find li", HxTrigger.FromFind "li" "")
[<Fact>]
let ``FromFind succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click")
[<Fact>]
let ``Target succeeds when it is the first modifier`` () =
Assert.Equal ("target:main", HxTrigger.Target "main" "")
[<Fact>]
let ``Target succeeds when it is not the first modifier`` () =
Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click")
[<Fact>]
let ``Consume succeeds when it is the first modifier`` () =
Assert.Equal ("consume", HxTrigger.Consume "")
[<Fact>]
let ``Consume succeeds when it is not the first modifier`` () =
Assert.Equal ("click consume", HxTrigger.Consume "click")
[<Fact>]
let ``Queue succeeds when it is the first modifier`` () =
Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "")
[<Fact>]
let ``Queue succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click")
[<Fact>]
let ``QueueFirst succeeds when it is the first modifier`` () =
Assert.Equal ("queue:first", HxTrigger.QueueFirst "")
[<Fact>]
let ``QueueFirst succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click")
[<Fact>]
let ``QueueLast succeeds when it is the first modifier`` () =
Assert.Equal ("queue:last", HxTrigger.QueueLast "")
[<Fact>]
let ``QueueLast succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:last", HxTrigger.QueueLast "click")
[<Fact>]
let ``QueueAll succeeds when it is the first modifier`` () =
Assert.Equal ("queue:all", HxTrigger.QueueAll "")
[<Fact>]
let ``QueueAll succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:all", HxTrigger.QueueAll "click")
[<Fact>]
let ``QueueNone succeeds when it is the first modifier`` () =
Assert.Equal ("queue:none", HxTrigger.QueueNone "")
[<Fact>]
let ``QueueNone succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:none", HxTrigger.QueueNone "click")
/// Tests for the HxVals module
module Vals =
[<Fact>]
let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxVals.From [])
[<Fact>]
let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }",
HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ])
/// Tests for the HtmxAttrs module
module Attributes =
/// Pipe-able assertion for a rendered node
let shouldRender expected node = Assert.Equal (expected, RenderView.AsString.htmlNode node)
[<Fact>]
let ``_hxBoost succeeds`` () =
div [ _hxBoost ] [] |> shouldRender """<div hx-boost="true"></div>"""
[<Fact>]
let ``_hxConfirm succeeds`` () =
button [ _hxConfirm "REALLY?!?" ] [] |> shouldRender """<button hx-confirm="REALLY?!?"></button>"""
[<Fact>]
let ``_hxDelete succeeds`` () =
span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """<span hx-delete="/this-endpoint"></span>"""
[<Fact>]
let ``_hxDisable succeeds`` () =
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
[<Fact>]
let ``_hxEncoding succeeds`` () =
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
[<Fact>]
let ``_hxExt succeeds`` () =
section [ _hxExt "extendme" ] [] |> shouldRender """<section hx-ext="extendme"></section>"""
[<Fact>]
let ``_hxGet succeeds`` () =
article [ _hxGet "/the-text" ] [] |> shouldRender """<article hx-get="/the-text"></article>"""
[<Fact>]
let ``_hxHeaders succeeds`` () =
figure [ _hxHeaders """{ "X-Special-Header": "some-header" }""" ] []
|> shouldRender """<figure hx-headers="{ &quot;X-Special-Header&quot;: &quot;some-header&quot; }"></figure>"""
[<Fact>]
let ``_hxHistoryElt succeeds`` () =
table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>"""
[<Fact>]
let ``_hxInclude succeeds`` () =
a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """<a hx-include=".extra-stuff"></a>"""
[<Fact>]
let ``_hxIndicator succeeds`` () =
aside [ _hxIndicator "#spinner" ] [] |> shouldRender """<aside hx-indicator="#spinner"></aside>"""
[<Fact>]
let ``_hxNoBoost succeeds`` () =
td [ _hxNoBoost ] [] |> shouldRender """<td hx-boost="false"></td>"""
[<Fact>]
let ``_hxParams succeeds`` () =
br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">"""
[<Fact>]
let ``_hxPatch succeeds`` () =
div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>"""
[<Fact>]
let ``_hxPost succeeds`` () =
hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">"""
[<Fact>]
let ``_hxPreserve succeeds`` () =
img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">"""
[<Fact>]
let ``_hxPrompt succeeds`` () =
strong [ _hxPrompt "Who goes there?" ] [] |> shouldRender """<strong hx-prompt="Who goes there?"></strong>"""
[<Fact>]
let ``_hxPushUrl succeeds`` () =
dl [ _hxPushUrl ] [] |> shouldRender """<dl hx-push-url></dl>"""
[<Fact>]
let ``_hxPut succeeds`` () =
s [ _hxPut "/take-this" ] [] |> shouldRender """<s hx-put="/take-this"></s>"""
[<Fact>]
let ``_hxRequest succeeds`` () =
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
[<Fact>]
let ``_hxSelect succeeds`` () =
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
[<Fact>]
let ``_hxSse succeeds`` () =
footer [ _hxSse "connect:/my-events" ] [] |> shouldRender """<footer hx-sse="connect:/my-events"></footer>"""
[<Fact>]
let ``_hxSwap succeeds`` () =
del [ _hxSwap "innerHTML" ] [] |> shouldRender """<del hx-swap="innerHTML"></del>"""
[<Fact>]
let ``_hxSwapOob succeeds`` () =
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
[<Fact>]
let ``_hxTarget succeeds`` () =
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
[<Fact>]
let ``_hxTrigger succeeds`` () =
figcaption [ _hxTrigger "load" ] [] |> shouldRender """<figcaption hx-trigger="load"></figcaption>"""
[<Fact>]
let ``_hxVals succeeds`` () =
dt [ _hxVals """{ "extra": "values" }""" ] []
|> shouldRender """<dt hx-vals="{ &quot;extra&quot;: &quot;values&quot; }"></dt>"""
[<Fact>]
let ``_hxWs succeeds`` () =
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""

View File

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description> <Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
@ -10,10 +9,16 @@
<ItemGroup> <ItemGroup>
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" 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" Version="1.4.0" />
<PackageReference Update="FSharp.Core" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -1,234 +1,531 @@
module Giraffe.ViewEngine.Htmx module Giraffe.ViewEngine.Htmx
/// Serialize a list of key/value pairs to JSON (very rudimentary) /// Serialize a list of key/value pairs to JSON (very rudimentary)
let private toJson (kvps : (string * string) list) = let private toJson (kvps : (string * string) list) =
kvps kvps
|> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\""))) |> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\"")))
|> String.concat ", " |> String.concat ", "
|> sprintf "{ %s }" |> sprintf "{ %s }"
/// Valid values for the `hx-encoding` attribute /// Valid values for the `hx-encoding` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxEncoding = module HxEncoding =
/// A standard HTTP form
let Form = "application/x-www-form-urlencoded" /// A standard HTTP form
/// A multipart form (used for file uploads) let Form = "application/x-www-form-urlencoded"
let MultipartForm = "multipart/form-data"
/// A multipart form (used for file uploads)
let MultipartForm = "multipart/form-data"
/// The events recognized by htmx
[<Struct>]
type HxEvent =
/// Send this event to an element to abort a request
| Abort
/// Triggered after an AJAX request has completed processing a successful response
| AfterOnLoad
/// Triggered after htmx has initialized a node
| AfterProcessNode
/// Triggered after an AJAX request has completed
| AfterRequest
/// Triggered after the DOM has settled
| AfterSettle
/// Triggered after new content has been swapped in
| AfterSwap
/// Triggered before htmx disables an element or removes it from the DOM
| BeforeCleanupElement
/// Triggered before any response processing occurs
| BeforeOnLoad
/// Triggered before htmx initializes a node
| BeforeProcessNode
/// Triggered before an AJAX request is made
| BeforeRequest
/// Triggered before a swap is done, allows you to configure the swap
| BeforeSwap
/// Triggered just before an ajax request is sent
| BeforeSend
/// Triggered before the request, allows you to customize parameters, headers
| ConfigRequest
/// Triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request
| Confirm
/// Triggered on an error during cache writing
| HistoryCacheError
/// Triggered on a cache miss in the history subsystem
| HistoryCacheMiss
/// Triggered on a unsuccessful remote retrieval
| HistoryCacheMissError
/// Triggered on a successful remote retrieval
| HistoryCacheMissLoad
/// Triggered when htmx handles a history restoration action
| HistoryRestore
/// Triggered before content is saved to the history cache
| BeforeHistorySave
/// Triggered when new content is added to the DOM
| Load
/// Triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined
| NoSseSourceError
/// Triggered when an exception occurs during the onLoad handling in htmx
| OnLoadError
/// Triggered after an out of band element as been swapped in
| OobAfterSwap
/// Triggered before an out of band element swap is done, allows you to configure the swap
| OobBeforeSwap
/// Triggered when an out of band element does not have a matching ID in the current DOM
| OobErrorNoTarget
/// Triggered after a prompt is shown
| Prompt
/// Triggered after an url is pushed into history
| PushedIntoHistory
/// Triggered when an HTTP response error (non-200 or 300 response code) occurs
| ResponseError
/// Triggered when a network error prevents an HTTP request from happening
| SendError
/// Triggered when an error occurs with a SSE source
| SseError
/// Triggered when a SSE source is opened
| SseOpen
/// Triggered when an error occurs during the swap phase
| SwapError
/// Triggered when an invalid target is specified
| TargetError
/// Triggered when a request timeout occurs
| Timeout
/// Triggered before an element is validated
| ValidationValidate
/// Triggered when an element fails validation
| ValidationFailed
/// Triggered when a request is halted due to validation errors
| ValidationHalted
/// Triggered when an ajax request aborts
| XhrAbort
/// Triggered when an ajax request ends
| XhrLoadEnd
/// Triggered when an ajax request starts
| XhrLoadStart
/// Triggered periodically during an ajax request that supports progress events
| XhrProgress
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
static member private Values = Map [
Abort, ("abort", "abort")
AfterOnLoad, ("afterOnLoad", "after-on-load")
AfterProcessNode, ("afterProcessNode", "after-process-node")
AfterRequest, ("afterRequest", "after-request")
AfterSettle, ("afterSettle", "after-settle")
AfterSwap, ("afterSwap", "after-swap")
BeforeCleanupElement, ("beforeCleanupElement", "before-cleanup-element")
BeforeOnLoad, ("beforeOnLoad", "before-on-load")
BeforeProcessNode, ("beforeProcessNode", "before-process-node")
BeforeRequest, ("beforeRequest", "before-request")
BeforeSwap, ("beforeSwap", "before-swap")
BeforeSend, ("beforeSend", "before-send")
ConfigRequest, ("configRequest", "config-request")
Confirm, ("confirm", "confirm")
HistoryCacheError, ("historyCacheError", "history-cache-error")
HistoryCacheMiss, ("historyCacheMiss", "history-cache-miss")
HistoryCacheMissError, ("historyCacheMissError", "history-cache-miss-error")
HistoryCacheMissLoad, ("historyCacheMissLoad", "history-cache-miss-load")
HistoryRestore, ("historyRestore", "history-restore")
BeforeHistorySave, ("beforeHistorySave", "before-history-save")
Load, ("load", "load")
NoSseSourceError, ("noSSESourceError", "no-sse-source-error")
OnLoadError, ("onLoadError", "on-load-error")
OobAfterSwap, ("oobAfterSwap", "oob-after-swap")
OobBeforeSwap, ("oobBeforeSwap", "oob-before-swap")
OobErrorNoTarget, ("oobErrorNoTarget", "oob-error-no-target")
Prompt, ("prompt", "prompt")
PushedIntoHistory, ("pushedIntoHistory", "pushed-into-history")
ResponseError, ("responseError", "response-error")
SendError, ("sendError", "send-error")
SseError, ("sseError", "sse-error")
SseOpen, ("sseOpen", "sse-open")
SwapError, ("swapError", "swap-error")
TargetError, ("targetError", "target-error")
Timeout, ("timeout", "timeout")
ValidationValidate, ("validation:validate", "validation:validate")
ValidationFailed, ("validation:failed", "validation:failed")
ValidationHalted, ("validation:halted", "validation:halted")
XhrAbort, ("xhr:abort", "xhr:abort")
XhrLoadEnd, ("xhr:loadend", "xhr:loadend")
XhrLoadStart, ("xhr:loadstart", "xhr:loadstart")
XhrProgress, ("xhr:progress", "xhr:progress")
]
/// The htmx event name
override this.ToString() = fst HxEvent.Values[this]
/// The hx-on variant of the htmx event name
member this.ToHxOnString() = snd HxEvent.Values[this]
/// Helper to create the `hx-headers` attribute /// Helper to create the `hx-headers` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxHeaders = module HxHeaders =
/// Create headers from a list of key/value pairs
let From = toJson /// Create headers from a list of key/value pairs
let From = toJson
/// Values / helpers for the `hx-params` attribute /// Values / helpers for the `hx-params` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxParams = module HxParams =
/// Include all parameters
let All = "*" /// Include all parameters
/// Include no parameters let All = "*"
let None = "none"
/// Include the specified parameters /// Include no parameters
let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") let None = "none"
/// Exclude the specified parameters
let Except fields = With fields |> sprintf "not %s" /// Include the specified parameters
let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
/// Exclude the specified parameters
let Except fields = With fields |> sprintf "not %s"
/// Helpers to define `hx-request` attribute values /// Helpers to define `hx-request` attribute values
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxRequest = module HxRequest =
/// Convert a boolean to its lowercase string equivalent
let private toLowerBool (it : bool) =
(string it).ToLowerInvariant ()
/// Configure the request with various options
let Configure (opts : string list) =
opts
|> String.concat ", "
|> sprintf "{ %s }"
/// Set a timeout (in milliseconds)
let Timeout (ms : int) = $"\"timeout\": {ms}"
/// Include or exclude credentials from the request
let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
/// Exclude or include headers from the request
let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
/// Convert a boolean to its lowercase string equivalent
let private toLowerBool (it : bool) =
(string it).ToLowerInvariant ()
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config) /// Configure the request with various options
[<RequireQualifiedAccess>] let Configure (opts : string list) =
module HxSwap = opts
/// The default, replace the inner html of the target element |> String.concat ", "
let InnerHtml = "innerHTML" |> sprintf "{ %s }"
/// Replace the entire target element with the response
let OuterHtml = "outerHTML" /// Set a timeout (in milliseconds)
/// Insert the response before the target element let Timeout (ms : int) = $"\"timeout\": {ms}"
let BeforeBegin = "beforebegin"
/// Insert the response before the first child of the target element /// Include or exclude credentials from the request
let AfterBegin = "afterbegin" let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
/// Insert the response after the last child of the target element
let BeforeEnd = "beforeend" /// Exclude or include headers from the request
/// Insert the response after the target element let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
let AfterEnd = "afterend"
/// Does not append content from response (out of band items will still be processed).
let None = "none"
/// Helpers for the `hx-trigger` attribute /// Helpers for the `hx-trigger` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxTrigger = module HxTrigger =
/// Append a filter to a trigger
let private appendFilter filter (trigger : string) = /// Append a filter to a trigger
match trigger.Contains "[" with let private appendFilter filter (trigger : string) =
| true -> match trigger.Contains "[" with
let parts = trigger.Split ('[', ']') | true ->
$"{parts.[0]}[{parts.[1]}&&{filter}]" let parts = trigger.Split ('[', ']')
| false -> $"{trigger}[{filter}]" $"{parts[0]}[{parts[1]}&&{filter}]"
/// Trigger the event on a click | false -> $"{trigger}[{filter}]"
let Click = "click"
/// Trigger the event on page load /// Trigger the event on a click
let Load = "load" let Click = "click"
/// Trigger the event when the item is visible
let Revealed = "revealed" /// Trigger the event on page load
/// Trigger this event every [timing declaration] let Load = "load"
let Every (duration : string) = $"every {duration}"
/// Helpers for defining filters /// Trigger the event when the item is visible
module Filter = let Revealed = "revealed"
/// Only trigger the event if the `ALT` key is pressed
let Alt = appendFilter "altKey" /// Trigger this event every [timing declaration]
/// Only trigger the event if the `CTRL` key is pressed let Every (duration : string) = $"every {duration}"
let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if the `SHIFT` key is pressed /// Helpers for defining filters
let Shift = appendFilter "shiftKey" module Filter =
/// Only trigger the event if `CTRL+ALT` are pressed
let CtrlAlt = Ctrl >> Alt /// Only trigger the event if the `ALT` key is pressed
/// Only trigger the event if `CTRL+SHIFT` are pressed let Alt = appendFilter "altKey"
let CtrlShift = Ctrl >> Shift
/// Only trigger the event if `CTRL+ALT+SHIFT` are pressed /// Only trigger the event if the `CTRL` key is pressed
let CtrlAltShift = CtrlAlt >> Shift let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if `ALT+SHIFT` are pressed
let AltShift = Alt >> Shift /// Only trigger the event if the `SHIFT` key is pressed
/// Append a modifier to the current trigger let Shift = appendFilter "shiftKey"
let private appendModifier modifier current =
match current with "" -> modifier | _ -> $"{current} {modifier}" /// Only trigger the event if `CTRL+ALT` are pressed
/// Only trigger once let CtrlAlt = Ctrl >> Alt
let Once = appendModifier "once"
/// Trigger when changed /// Only trigger the event if `CTRL+SHIFT` are pressed
let Changed = appendModifier "changed" let CtrlShift = Ctrl >> Shift
/// Delay execution; resets every time the event is seen
let Delay = sprintf "delay:%s" >> appendModifier /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed
/// Throttle execution; ignore other events, fire when duration passes let CtrlAltShift = CtrlAlt >> Shift
let Throttle = sprintf "throttle:%s" >> appendModifier
/// Trigger this event from a CSS selector /// Only trigger the event if `ALT+SHIFT` are pressed
let From = sprintf "from:%s" >> appendModifier let AltShift = Alt >> Shift
/// Trigger this event from the `document` object
let FromDocument = From "document" /// Append a modifier to the current trigger
/// Trigger this event from the `window` object let private appendModifier modifier current =
let FromWindow = From "window" if current = "" then modifier else $"{current} {modifier}"
/// Trigger this event from the closest parent CSS selector
let FromClosest = sprintf "closest %s" >> From /// Only trigger once
/// Trigger this event from the closest child CSS selector let Once = appendModifier "once"
let FromFind = sprintf "find %s" >> From
/// Target the given CSS selector with the results of this event /// Trigger when changed
let Target = sprintf "target:%s" >> appendModifier let Changed = appendModifier "changed"
/// Prevent any further events from occurring after this one fires
let Consume = appendModifier "consume" /// Delay execution; resets every time the event is seen
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" let Delay = sprintf "delay:%s" >> appendModifier
let Queue = sprintf "queue:%s" >> appendModifier
/// Queue the first event, discard all others (i.e., a FIFO queue of 1) /// Throttle execution; ignore other events, fire when duration passes
let QueueFirst = Queue "first" let Throttle = sprintf "throttle:%s" >> appendModifier
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
let QueueLast = Queue "last" /// Trigger this event from a CSS selector
/// Queue all events; discard none let From = sprintf "from:%s" >> appendModifier
let QueueAll = Queue "all"
/// Queue no events; discard all /// Trigger this event from the `document` object
let QueueNone = Queue "none" let FromDocument = From "document"
/// Trigger this event from the `window` object
let FromWindow = From "window"
/// Trigger this event from the closest parent CSS selector
let FromClosest = sprintf "closest %s" >> From
/// Trigger this event from the closest child CSS selector
let FromFind = sprintf "find %s" >> From
/// Target the given CSS selector with the results of this event
let Target = sprintf "target:%s" >> appendModifier
/// Prevent any further events from occurring after this one fires
let Consume = appendModifier "consume"
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last"
let Queue = sprintf "queue:%s" >> appendModifier
/// Queue the first event, discard all others (i.e., a FIFO queue of 1)
let QueueFirst = Queue "first"
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
let QueueLast = Queue "last"
/// Queue all events; discard none
let QueueAll = Queue "all"
/// Queue no events; discard all
let QueueNone = Queue "none"
/// Helper to create the `hx-vals` attribute /// Helper to create the `hx-vals` attribute
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxVals = module HxVals =
/// Create values from a list of key/value pairs
let From = toJson /// Create values from a list of key/value pairs
let From = toJson
/// Attributes and flags for HTMX /// Attributes and flags for htmx
[<AutoOpen>] [<AutoOpen>]
module HtmxAttrs = module HtmxAttrs =
/// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
let _hxBoost = attr "hx-boost" "true" /// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
/// Shows a confirm() dialog before issuing a request let _hxBoost = attr "hx-boost" "true"
let _hxConfirm = attr "hx-confirm"
/// Issues a DELETE to the specified URL /// Shows a confirm() dialog before issuing a request
let _hxDelete = attr "hx-delete" let _hxConfirm = attr "hx-confirm"
/// Disables htmx processing for the given node and any children nodes
let _hxDisable = flag "hx-disable" /// Issues a DELETE to the specified URL
/// Changes the request encoding type let _hxDelete = attr "hx-delete"
let _hxEncoding = attr "hx-encoding"
/// Extensions to use for this element /// Disables htmx processing for the given node and any children nodes
let _hxExt = attr "hx-ext" let _hxDisable = flag "hx-disable"
/// Issues a GET to the specified URL
let _hxGet = attr "hx-get" /// Specifies elements that should be disabled when an htmx request is in flight
/// Adds to the headers that will be submitted with the request let _hxDisabledElt = attr "hx-disabled-elt"
let _hxHeaders = attr "hx-headers"
/// The element to snapshot and restore during history navigation /// Disinherit all ("*") or specific htmx attributes
let _hxHistoryElt = flag "hx-history-elt" let _hxDisinherit = attr "hx-disinherit"
/// Includes additional data in AJAX requests
let _hxInclude = attr "hx-include" /// Changes the request encoding type
/// The element to put the htmx-request class on during the AJAX request let _hxEncoding = attr "hx-encoding"
let _hxIndicator = attr "hx-indicator"
/// Overrides a previous `hx-boost` /// Extensions to use for this element
let _hxNoBoost = attr "hx-boost" "false" let _hxExt = attr "hx-ext"
/// Filters the parameters that will be submitted with a request
let _hxParams = attr "hx-params" /// Issues a GET to the specified URL
/// Issues a PATCH to the specified URL let _hxGet = attr "hx-get"
let _hxPatch = attr "hx-patch"
/// Issues a POST to the specified URL /// Adds to the headers that will be submitted with the request
let _hxPost = attr "hx-post" let _hxHeaders = attr "hx-headers"
/// Preserves an element between requests
let _hxPreserve = attr "hx-preserve" "true" /// Set to "false" to prevent pages with sensitive information from being stored in the history cache
/// Shows a prompt before submitting a request let _hxHistory = attr "hx-history"
let _hxPrompt = attr "hx-prompt"
/// Pushes the URL into the location bar, creating a new history entry /// The element to snapshot and restore during history navigation
let _hxPushUrl = flag "hx-push-url" let _hxHistoryElt = flag "hx-history-elt"
/// Issues a PUT to the specified URL
let _hxPut = attr "hx-put" /// Includes additional data in AJAX requests
/// Configures various aspects of the request let _hxInclude = attr "hx-include"
let _hxRequest = attr "hx-request"
/// Selects a subset of the server response to process /// The element to put the htmx-request class on during the AJAX request
let _hxSelect = attr "hx-select" let _hxIndicator = attr "hx-indicator"
/// Establishes and listens to Server Sent Event (SSE) sources for events
let _hxSse = attr "hx-sse" /// Overrides a previous `hx-boost`
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd') let _hxNoBoost = attr "hx-boost" "false"
let _hxSwap = attr "hx-swap"
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target /// Attach an event handler for DOM events
let _hxSwapOob = attr "hx-swap-oob" let _hxOnEvent evtName =
/// Specifies the target element to be swapped attr $"hx-on:%s{evtName}"
let _hxTarget = attr "hx-target"
/// Specifies the event that triggers the request /// Attach an event handler for htmx events
let _hxTrigger = attr "hx-trigger" let _hxOnHxEvent (hxEvent: HxEvent) =
/// Adds to the parameters that will be submitted with the request _hxOnEvent $":{hxEvent.ToHxOnString()}"
let _hxVals = attr "hx-vals"
/// Establishes a WebSocket or sends information to one /// Filters the parameters that will be submitted with a request
let _hxWs = attr "hx-ws" let _hxParams = attr "hx-params"
/// Issues a PATCH to the specified URL
let _hxPatch = attr "hx-patch"
/// Issues a POST to the specified URL
let _hxPost = attr "hx-post"
/// Preserves an element between requests
let _hxPreserve = attr "hx-preserve" "true"
/// Shows a prompt before submitting a request
let _hxPrompt = attr "hx-prompt"
/// Pushes the URL into the location bar, creating a new history entry
let _hxPushUrl = attr "hx-push-url"
/// Issues a PUT to the specified URL
let _hxPut = attr "hx-put"
/// Replaces the current URL in the browser's history stack
let _hxReplaceUrl = attr "hx-replace-url"
/// Configures various aspects of the request
let _hxRequest = attr "hx-request"
/// Selects a subset of the server response to process
let _hxSelect = attr "hx-select"
/// Selects a subset of an out-of-band server response
let _hxSelectOob = attr "hx-select-oob"
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd')
let _hxSwap = attr "hx-swap"
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd'), enabling CSS
/// transitions
let _hxSwapWithTransition = sprintf "%s transition:true" >> _hxSwap
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
let _hxSwapOob = attr "hx-swap-oob"
/// Synchronize events based on another element
let _hxSync = attr "hx-sync"
/// Specifies the target element to be swapped
let _hxTarget = attr "hx-target"
/// Specifies the event that triggers the request
let _hxTrigger = attr "hx-trigger"
/// Validate an input element (uses HTML5 validation API)
let _hxValidate = flag "hx-validate"
/// Adds to the parameters that will be submitted with the request
let _hxVals = attr "hx-vals"
/// The name of the message to swap into the DOM.
let _sseSwap = attr "sse-swap"
/// The URL of the SSE server.
let _sseConnect = attr "sse-connect"
/// Script tags to pull htmx into an web page /// Script tags to pull htmx into a web page
module Script = module Script =
/// Script tag to load the minified version from unpkg.com /// Script tag to load the minified version from unpkg.com
let minified = let minified =
script [ script [ _src "https://unpkg.com/htmx.org@2.0.2"
_src "https://unpkg.com/htmx.org@1.6.1" _integrity "sha384-Y7hw+L/jvKeWIRRkqWYfPcvVxHzVzn5REgzbawhxAuQGwX1XWe70vji+VSeHOThJ"
_integrity "sha384-tvG/2mnCFmGQzYC1Oh3qxQ7CkQ9kMzYjWZSNtrRZygHPDDqottzEJsqS4oUVodhW" _crossorigin "anonymous" ] []
_crossorigin "anonymous"
] []
/// Script tag to load the unminified version from unpkg.com /// Script tag to load the unminified version from unpkg.com
let unminified = let unminified =
script [ script [ _src "https://unpkg.com/htmx.org@2.0.2/dist/htmx.js"
_src "https://unpkg.com/htmx.org@1.6.1/dist/htmx.js" _integrity "sha384-yZq+5izaUBKcRgFbxgkRYwpHhHHCpp5nseXp0MEQ1A4MTWVMnqkmcuFez8x5qfxr"
_integrity "sha384-7G9OE6gS4pBnBGH74HojjPQ8xOEGrdBeQc7JJOc58k6LG/YVfKXARd91w9715AYG" _crossorigin "anonymous" ] []
_crossorigin "anonymous"
] []
/// Functions to extract and render an HTML fragment from a document
[<RequireQualifiedAccess>]
module RenderFragment =
/// Does this element have an ID matching the requested ID name?
let private isIdElement nodeId (elt : XmlElement) =
snd elt
|> Array.exists (fun attr ->
match attr with
| KeyValue (name, value) -> name = "id" && value = nodeId
| Boolean _ -> false)
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// Find the node with the named ID
let rec findIdNode nodeId (node : XmlNode) : XmlNode option =
match node with
| Text _ -> None
| VoidElement elt -> if isIdElement nodeId elt then Some node else None
| ParentNode (elt, children) ->
if isIdElement nodeId elt then Some node else children |> List.tryPick (fun c -> findIdNode nodeId c)
/// Functions to render a fragment as a string
[<RequireQualifiedAccess>]
module AsString =
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Functions to render a fragment as bytes
[<RequireQualifiedAccess>]
module AsBytes =
let private utf8 = System.Text.Encoding.UTF8
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Functions to render a fragment into a StringBuilder
[<RequireQualifiedAccess>]
module IntoStringBuilder =
/// Render to HTML for the given ID
let htmlFromNodes sb nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore
/// Render to HTML for the given ID
let htmlFromNode sb nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore

View File

@ -2,7 +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: 1.6.1** **htmx version: 2.0.2**
_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_
### Setup ### Setup
@ -14,7 +16,7 @@ This package enables [htmx](https://htmx.org) support within the [Giraffe](https
Following Giraffe View Engine's lead, there are a set of attribute functions for htmx; for many of the attributes, there are also helper modules to assist with typing the values. The example below utilizes both: Following Giraffe View Engine's lead, there are a set of attribute functions for htmx; for many of the attributes, there are also helper modules to assist with typing the values. The example below utilizes both:
```fsharp ```fsharp
let autoload = let autoload =
div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ] div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ]
``` ```
@ -23,31 +25,39 @@ Support modules include:
- `HxHeaders` - `HxHeaders`
- `HxParams` - `HxParams`
- `HxRequest` - `HxRequest`
- `HxSwap` - `HxSwap` (requires `open Giraffe.Htmx`)
- `HxTrigger` - `HxTrigger`
- `HxVals` - `HxVals`
There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging). There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging).
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).
### Learn ### Learn
htmx's attributes and these attribute functions map one-to-one. The lone exception is `_hxBoost`, which implies `true`; use `_hxNoBoost` to set it to `false`. 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: htmx's attributes and these attribute functions map one-to-one. There are two exceptions:
- `_hxBoost` implies `true`; use `_hxNoBoost` to set it to `false`.
- `_hxSwapWithTransition` renders the standard `hx-swap` attribute and appends `transition:true` to the specified swap value.
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:
- `HxRequest` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request. - `HxRequest` 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 HxRequest.Configure [ HxRequest.Timeout 500 ] |> _hxRequest
``` ```
- `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:
```fsharp ```fsharp
HxTrigger.Click HxTrigger.Click
|> HxTrigger.Filter.Shift |> HxTrigger.Filter.Shift
|> HxTrigger.FromDocument |> HxTrigger.FromDocument
|> HxTrigger.Delay "3s" |> HxTrigger.Delay "3s"
|> _hxTrigger |> _hxTrigger
// or // or
(HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click (HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click
|> _hxTrigger |> _hxTrigger
``` ```

BIN
src/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

7
src/pack.sh Executable file
View File

@ -0,0 +1,7 @@
#!/bin/bash
dotnet pack Common/Giraffe.Htmx.Common.fsproj -c Release
dotnet pack Htmx/Giraffe.Htmx.fsproj -c Release
dotnet pack ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj -c Release
cp Common/bin/Release/Giraffe.Htmx.Common.*.nupkg .
cp Htmx/bin/Release/Giraffe.Htmx.*.nupkg .
cp ViewEngine.Htmx/bin/Release/Giraffe.ViewEngine.Htmx.*.nupkg .