Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a762fbdd5 | ||
|
|
59b64ed834 | ||
|
|
5c12ad7cef | ||
|
|
d4a7e0c9ce | ||
|
|
de75672bb3 | ||
|
|
121eb95d87 | ||
|
|
6b7458070b | ||
|
|
10c31d77b5 | ||
|
|
f4cd184a15 | ||
|
|
5626031593 | ||
|
|
f0de18845f | ||
|
|
961307fd99 | ||
|
|
a2960a79c6 | ||
|
|
541384a92f | ||
|
|
8cb5d6bfa7 | ||
|
|
1a11e3511a | ||
|
|
32e962416d | ||
|
|
29839fa795 | ||
|
|
7f9b3a6234 | ||
|
|
a8d2b819dc | ||
|
|
1ea05b79ed | ||
|
|
4f6bb8367a | ||
|
|
b3665a4b72 | ||
|
|
bdb7255a1c | ||
|
|
9276db7ffe | ||
|
|
94b68f76c9 | ||
|
|
90de16529c | ||
|
|
452f15b2d4 | ||
|
|
59246ae7f5 | ||
|
|
16355e8f58 | ||
|
|
71286b9064 | ||
|
|
85ac22877c | ||
|
|
dea2323499 | ||
|
|
fcaaa693bc | ||
|
|
8fd6af8c26 | ||
|
|
69a4034661 | ||
|
|
5aa9408e60 | ||
|
|
98f53a5e53 | ||
|
|
7798314fb8 | ||
|
|
50c66435e8 | ||
|
|
5b3a1be87e | ||
|
|
168f434030 | ||
|
|
e3db0bced0 | ||
|
|
fb5c4f1bcb | ||
|
|
c3229d51d1 | ||
|
|
951edef8ad | ||
|
|
3908451d6e | ||
|
|
277d93dd99 | ||
|
|
061f6e5a4e | ||
|
|
bb2df73175 | ||
|
|
e0c567098d | ||
|
|
4be5bad8ef | ||
|
|
a169000ce2 | ||
|
|
c587a28770 | ||
|
|
b5292bffc4 | ||
|
|
86defea3c1 | ||
|
|
9a9f159cab | ||
|
|
9fcba06e75 | ||
|
|
5906f3b295 | ||
|
|
dc06b06b1f |
@@ -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
|
||||||
@@ -5,3 +5,7 @@
|
|||||||
.ionide
|
.ionide
|
||||||
.idea
|
.idea
|
||||||
*.user
|
*.user
|
||||||
|
.vscode
|
||||||
|
src/*.nupkg
|
||||||
|
src/tests*.txt
|
||||||
|
|
||||||
|
|||||||
@@ -10,46 +10,53 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
|
|||||||
|
|
||||||
`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods.
|
`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods.
|
||||||
|
|
||||||
| Server Side | View Engine |
|
| Server Side | View Engine |
|
||||||
|---|---|
|
|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
|[](https://www.nuget.org/packages/Giraffe.Htmx/)|[](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)|
|
| [](https://www.nuget.org/packages/Giraffe.Htmx/) | [](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/) |
|
||||||
|
|
||||||
|
Both of these packages will also install `Giraffe.Htmx.Common`, which has some common definitions and provides a local-to-your-project version of the htmx JavaScript _(as of v2.0.8)_ and htmax htmx-plus-extensions bundle _(as of v4.0.0-beta4)_.
|
||||||
|
|
||||||
## Server Side (`Giraffe.Htmx`)
|
## Server Side (`Giraffe.Htmx`)
|
||||||
|
|
||||||
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request.
|
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://four.htmx.org/reference/headers#request) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request.
|
||||||
|
|
||||||
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://four.htmx.org/reference/headers#response) 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 `fetch` handler on the client may 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.
|
||||||
|
|
||||||
|
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library, along with `localMax` to load the htmx-plus-extensions bundle. This can be used in code, Razor templates, etc. (If you're using Giraffe.ViewEngine, see below.)
|
||||||
|
|
||||||
## View Engine (`Giraffe.ViewEngine.Htmx`)
|
## View Engine (`Giraffe.ViewEngine.Htmx`)
|
||||||
|
|
||||||
As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible.
|
As htmx uses [attributes](https://four.htmx.org/reference/attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible.
|
||||||
|
|
||||||
As 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,18 +64,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 use the package-provided htmx library, `Htmx.Script.local` will create the `script` tag for you (as well as `Htmx.Script.Max.local` to load the htmax bundle). To load htmx from jsDelivr, `Htmx.Script.cdnMinified` or `Htmx.Script.cdnUnminified` can be used to load the script in your HTML trees (these also have `.Max` variants). In this case, if you are using a Content Security Policy (CSP) header, `cdn.jsdelivr.net` will need to be added to the `script-src` list.
|
||||||
|
|
||||||
## 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 (and most others) of the [F# Community Discord server](https://discord.gg/R6n7c54).
|
||||||
|
|
||||||
## Thanks
|
## Thanks
|
||||||
|[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)|[<img src="https://bitbadger.solutions/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|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
/// <summary>Common definitions shared between attribute values and response headers</summary>
|
||||||
|
[<AutoOpen>]
|
||||||
|
module Giraffe.Htmx.Common
|
||||||
|
|
||||||
|
/// <summary>The version of htmx embedded in the package</summary>
|
||||||
|
let HtmxVersion = "4.0.0"
|
||||||
|
|
||||||
|
/// <summary>URLs for the included htmx library static web assets</summary>
|
||||||
|
module StaticAssetUrl =
|
||||||
|
/// <summary>The path for the provided htmx script</summary>
|
||||||
|
let htmx = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
|
||||||
|
|
||||||
|
/// <summary>The path for the provided htmax script</summary>
|
||||||
|
let htmax = $"/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"
|
||||||
|
|
||||||
|
/// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary>
|
||||||
|
/// <param name="pairs">The key/value pairs to be serialized to JSON</param>
|
||||||
|
/// <returns>A string with the key/value pairs serialized to JSON</returns>
|
||||||
|
let internal toJson (pairs: (string * string) list) =
|
||||||
|
pairs
|
||||||
|
|> List.map (fun pair -> sprintf "\"%s\": \"%s\"" (fst pair) ((snd pair).Replace ("\"", "\\\"")))
|
||||||
|
|> String.concat ", "
|
||||||
|
|> sprintf "{ %s }"
|
||||||
|
|
||||||
|
/// <summary>Convert a boolean to lowercase "true" or "false"</summary>
|
||||||
|
/// <param name="boolValue">The boolean value to convert</param>
|
||||||
|
/// <returns>"true" for <c>true</c>, "false" for <c>false</c></returns>
|
||||||
|
let internal toLowerBool (boolValue: bool) =
|
||||||
|
(string boolValue).ToLowerInvariant()
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Valid values for the <c>hx-swap</c> attribute / <c>HX-Reswap</c> header</summary>
|
||||||
|
/// <remarks>May be combined with <c>swap</c> / <c>scroll</c> / <c>show</c> config)</remarks>
|
||||||
|
/// <seealso href="https://four.htmx.org/attributes/hx-swap/">Documentation</seealso>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxSwap =
|
||||||
|
|
||||||
|
/// <summary>The default, replace the inner HTML of the target element</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let InnerHtml = "innerHTML"
|
||||||
|
|
||||||
|
/// <summary>Replace the entire target element with the response</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let OuterHtml = "outerHTML"
|
||||||
|
|
||||||
|
/// <summary>Morph the inner HTML of the target to the new content</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let InnerMorph = "innerMorph"
|
||||||
|
|
||||||
|
/// <summary>Morph the outer HTML of the target to the new content</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let OuterMorph = "outerMorph"
|
||||||
|
|
||||||
|
/// <summary>Morph the outer HTML of the target to the new content, recreating all children</summary>
|
||||||
|
/// <remarks>This is used internally by the new history extension, but can be used by others if desired</remarks>
|
||||||
|
[<Literal>]
|
||||||
|
let OuterSync = "outerSync"
|
||||||
|
|
||||||
|
/// <summary>Replace the text content of the target without parsing the response as HTML</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let TextContent = "textContent"
|
||||||
|
|
||||||
|
/// <summary>Insert the response before the target element</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Before = "before"
|
||||||
|
|
||||||
|
/// <summary>Insert the response before the target element (pre-v4 name)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let BeforeBegin = Before
|
||||||
|
|
||||||
|
/// <summary>Insert the response before the first child of the target element</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Prepend = "prepend"
|
||||||
|
|
||||||
|
/// <summary>Insert the response before the first child of the target element (pre-v4 name)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let AfterBegin = Prepend
|
||||||
|
|
||||||
|
/// <summary>Insert the response after the last child of the target element</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Append = "append"
|
||||||
|
|
||||||
|
/// <summary>Insert the response after the last child of the target element (pre-v4 name)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let BeforeEnd = Append
|
||||||
|
|
||||||
|
/// <summary>Insert the response after the target element</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let After = "after"
|
||||||
|
|
||||||
|
/// <summary>Insert the response after the target element (pre-v4 name)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let AfterEnd = After
|
||||||
|
|
||||||
|
/// <summary>Delete the target element regardless of response</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Delete = "delete"
|
||||||
|
|
||||||
|
/// <summary>Does not append content from response (out of band items will still be processed)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let None = "none"
|
||||||
|
|
||||||
|
/// <summary>Update existing elements by <c>id</c> and add new ones</summary>
|
||||||
|
/// <remarks>This requires the <c>upsert</c> extension</remarks>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/upsert">Extension</seealso>
|
||||||
|
[<Literal>]
|
||||||
|
let Upsert = "upsert"
|
||||||
|
|
||||||
|
/// <summary>Specify that the target of the htmx request should be downloaded</summary>
|
||||||
|
/// <remarks>This requires the <c>hx-download</c> extension (included in the htmax bundle)</remarks>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-download#explicit-swap-style">Documentation</seealso>
|
||||||
|
[<Literal>]
|
||||||
|
let Download = "download"
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<Description>Common definitions for Giraffe.Htmx</Description>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="Common.fs" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||||
|
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="FSharp.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Giraffe.Htmx" />
|
||||||
|
<InternalsVisibleTo Include="Giraffe.ViewEngine.Htmx" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## Giraffe.Htmx.Common
|
||||||
|
|
||||||
|
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx and htmax as static web assets, allowing them to be loaded from your local (or published) project.
|
||||||
|
|
||||||
|
**htmx version: 4.0.0**
|
||||||
|
|
||||||
|
_**NOTE:** Pay special attention to breaking changes highlighted in the packages listed above._
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,13 +1,18 @@
|
|||||||
<?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>0.9.1</VersionPrefix>
|
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||||
<PackageReleaseNotes>Initial NuGet release</PackageReleaseNotes>
|
<VersionPrefix>4.0.0</VersionPrefix>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageReleaseNotes>htmx 4.0.0 Initial Release
|
||||||
|
|
||||||
|
See package and prior alpha/beta release READMEs; v2 to v4 is not an update-and-forget-it release. If you encounter build errors after installing this, install v4.0.0-beta5 instead; obsolete elements will generate a warning with suggestions of what to use instead.
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Expecto" Version="11.1.0" />
|
||||||
|
<PackageVersion Include="Giraffe" Version="8.3.0" />
|
||||||
|
<PackageVersion Include="Giraffe.ViewEngine" Version="1.4.0" />
|
||||||
|
<PackageVersion Include="NSubstitute" Version="6.2.0" />
|
||||||
|
<PackageVersion Include="FSharp.Core" Version="10.1.400" />
|
||||||
|
<!-- <PackageVersion Update="FSharp.Core" Version="10.0.0" /> -->
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="Common/Giraffe.Htmx.Common.fsproj" />
|
||||||
|
<Project Path="Htmx/Giraffe.Htmx.fsproj" />
|
||||||
|
<Project Path="Tests/Tests.fsproj" />
|
||||||
|
<Project Path="ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj" />
|
||||||
|
</Solution>
|
||||||
@@ -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>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module Program = let [<EntryPoint>] main _ = 0
|
|
||||||
@@ -1,318 +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 ``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])
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,17 +1,25 @@
|
|||||||
<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>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Htmx.fs" />
|
<Compile Include="Htmx.fs" />
|
||||||
|
<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" />
|
||||||
|
<PackageReference Include="FSharp.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+175
-60
@@ -1,99 +1,214 @@
|
|||||||
module Giraffe.Htmx
|
module Giraffe.Htmx
|
||||||
|
|
||||||
open Microsoft.AspNetCore.Http
|
open Microsoft.AspNetCore.Http
|
||||||
open Microsoft.Extensions.Primitives
|
open Microsoft.Extensions.Primitives
|
||||||
open System
|
open System
|
||||||
|
|
||||||
|
/// <summary>The request types which may be set in the <c>HX-Request</c> header</summary>
|
||||||
|
type HxRequestTypes =
|
||||||
|
/// <summary>A request targeting the <c>body</c> tag or using an <c>hx-select</c> attribute</summary>
|
||||||
|
| HxFullRequest
|
||||||
|
|
||||||
|
/// <summary>A request for partial content</summary>
|
||||||
|
| HxPartialRequest
|
||||||
|
|
||||||
/// Determine if the given header is present
|
/// Determine if the given header is present
|
||||||
let private hdr (headers : IHeaderDictionary) hdr =
|
let private hdr (headers : IHeaderDictionary) hdr =
|
||||||
match headers.[hdr] with it when it = StringValues.Empty -> None | it -> Some it.[0]
|
match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0]
|
||||||
|
|
||||||
/// 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`
|
/// <summary>Indicates that the request is via an element using <c>hx-boost</c></summary>
|
||||||
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)_
|
/// <summary>The current URL of the browser <em>(note that this does not update until after settle)</em></summary>
|
||||||
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
|
/// <summary><c>true</c> if the request is for history restoration after a miss in the local history cache</summary>
|
||||||
member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
|
member this.HxHistoryRestoreRequest
|
||||||
|
with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
|
||||||
|
|
||||||
/// The user response to an `hx-prompt`
|
/// <summary><c>true</c> if the request has been fired by the preload extension</summary>
|
||||||
member this.HxPrompt with get () = hdr this "HX-Prompt"
|
/// <remarks><c>preload</c> is part of the htmax htmx-plus-extensions bundle</remarks>
|
||||||
|
member this.HxPreloaded
|
||||||
|
with get () = hdr this "HX-Preloaded" |> Option.map bool.Parse
|
||||||
|
|
||||||
/// `true` if the request came from HTMX
|
/// <summary>The user response to an <c>hx-prompt</c></summary>
|
||||||
member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse
|
/// <remarks><b>NEW IN v4:</b> This functionality is dependent on the hx-prompt extension being loaded</remarks>
|
||||||
|
member this.HxPrompt
|
||||||
|
with get () = hdr this "HX-Prompt"
|
||||||
|
|
||||||
/// The `id` of the target element if it exists
|
/// <summary><c>true</c> if the request came from htmx</summary>
|
||||||
member this.HxTarget with get () = hdr this "HX-Target"
|
member this.HxRequest
|
||||||
|
with get () = hdr this "HX-Request" |> Option.map bool.Parse
|
||||||
|
|
||||||
/// The `id` of the triggered element if it exists
|
/// <summary>The ID of the request (WebSocket extension requests only)</summary>
|
||||||
member this.HxTrigger with get () = hdr this "HX-Trigger"
|
/// <remarks><c>hx-ws</c> is part of the htmax htmx-plus-extensions bundle</remarks>
|
||||||
|
member this.HxRequestId
|
||||||
|
with get () = hdr this "HX-Request-ID"
|
||||||
|
|
||||||
/// The `name` of the triggered element if it exists
|
/// <summary>The request type sent by htmx</summary>
|
||||||
member this.HxTriggerName with get () = hdr this "HX-Trigger-Name"
|
/// <seealso cref="HxRequestTypes" />
|
||||||
|
member this.HxRequestType
|
||||||
|
with get () =
|
||||||
|
match hdr this "HX-Request-Type" with
|
||||||
|
| Some typ when typ = "full" -> Some HxFullRequest
|
||||||
|
| Some typ when typ = "partial" -> Some HxPartialRequest
|
||||||
|
| Some _ -> None
|
||||||
|
| None -> None
|
||||||
|
|
||||||
|
/// <summary>The tag name (fst) and <c>id</c> attribute (snd) of the element triggering this request</summary>
|
||||||
|
member this.HxSource
|
||||||
|
with get () =
|
||||||
|
match hdr this "HX-Source" with
|
||||||
|
| Some src ->
|
||||||
|
let parts = src.Split "#"
|
||||||
|
if parts.Length = 1 then
|
||||||
|
Some (parts[0], None)
|
||||||
|
else
|
||||||
|
Some (parts[0], if parts[1] <> "" then Some parts[1] else None)
|
||||||
|
| None -> None
|
||||||
|
|
||||||
|
/// <summary>The <c>id</c> attribute of the target element if it exists</summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In v4, this changed to tag name (fst) and <c>id</c> (snd); to resolve build errors, and restore the prior
|
||||||
|
/// behavior of <c>[id] option</c>, replace
|
||||||
|
/// <code>ctx.HxTarget</code>
|
||||||
|
/// with
|
||||||
|
/// <code>ctx.HxTarget |> Option.iter snd</code>
|
||||||
|
/// </remarks>
|
||||||
|
member this.HxTarget
|
||||||
|
with get () =
|
||||||
|
match hdr this "HX-Target" with
|
||||||
|
| Some src ->
|
||||||
|
let parts = src.Split "#"
|
||||||
|
if parts.Length = 1 then
|
||||||
|
Some (parts[0], None)
|
||||||
|
else
|
||||||
|
Some (parts[0], if parts[1] <> "" then Some parts[1] else None)
|
||||||
|
| None -> None
|
||||||
|
|
||||||
|
|
||||||
/// Extensions for the request object
|
/// Extensions for the request object
|
||||||
type HttpRequest with
|
type HttpRequest with
|
||||||
|
|
||||||
/// Whether this request was initiated from HTMX
|
/// <summary>Whether this request was initiated from htmx</summary>
|
||||||
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
|
/// <summary>Whether this request is an htmx history-miss refresh request</summary>
|
||||||
member this.IsHtmxRefresh with get () =
|
member this.IsHtmxRefresh
|
||||||
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
|
with get () = this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
|
||||||
|
|
||||||
|
|
||||||
/// HTTP handlers for setting output headers
|
/// <summary>HTTP handlers for setting output headers</summary>
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module Handlers =
|
module Handlers =
|
||||||
|
|
||||||
/// Convert a boolean to lowercase `true` or `false`
|
open Giraffe.Htmx.Common
|
||||||
let private toLowerBool (trueOrFalse : bool) =
|
|
||||||
(string trueOrFalse).ToLowerInvariant ()
|
|
||||||
|
|
||||||
/// Serialize a list of key/value pairs to JSON (very rudimentary)
|
/// <summary>Instruct htmx to download a response from another path / URL</summary>
|
||||||
let private toJson (evts : (string * string) list) =
|
/// <param name="path">The path or URL where the downloadable content is found</param>
|
||||||
evts
|
/// <returns>An HTTP handler with the <c>HX-Download</c> header set</returns>
|
||||||
|> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\"")))
|
/// <remarks>This requires the client-side <c>hx-download</c> extension (included in the htmax bundle)</remarks>
|
||||||
|> String.concat ", "
|
/// <seealso href="https://four.htmx.org/extensions/hx-download#hx-download-header">Documentation</seealso>
|
||||||
|> sprintf "{ %s }"
|
let withHxDownload (path: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Download" path
|
||||||
|
|
||||||
// Pushes a new url into the history stack
|
/// <summary>Instruct htmx to perform a client-side redirect for content</summary>
|
||||||
let withHxPush : string -> HttpHandler =
|
/// <param name="path">The path where the content should be found</param>
|
||||||
setHttpHeader "HX-Push"
|
/// <returns>An HTTP handler with the <c>HX-Location</c> header set</returns>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-location/">Documentation</seealso>
|
||||||
|
let withHxLocation (path: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Location" path
|
||||||
|
|
||||||
/// Can be used to do a client-side redirect to a new location
|
/// <summary>Pushes a new url into the history stack</summary>
|
||||||
let withHxRedirect : string -> HttpHandler =
|
/// <param name="url">The URL to be pushed</param>
|
||||||
setHttpHeader "HX-Redirect"
|
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set</returns>
|
||||||
|
/// <remarks>Use <see cref="withHxNoPushUrl" /> to explicitly not push a new URL</remarks>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
|
||||||
|
let withHxPushUrl (url: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Push-Url" url
|
||||||
|
|
||||||
/// If set to `true` the client side will do a a full refresh of the page
|
/// <summary>Explicitly do not push a new URL into the history stack</summary>
|
||||||
let withHxRefresh : bool -> HttpHandler =
|
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set to <c>false</c></returns>
|
||||||
toLowerBool >> setHttpHeader "HX-Refresh"
|
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
|
||||||
|
let withHxNoPushUrl : HttpHandler =
|
||||||
|
toLowerBool false |> withHxPushUrl
|
||||||
|
|
||||||
/// Allows you to trigger a single client side event
|
/// <summary>Can be used to do a client-side redirect to a new location</summary>
|
||||||
let withHxTrigger : string -> HttpHandler =
|
/// <param name="url">The URL to which the client should be redirected</param>
|
||||||
setHttpHeader "HX-Trigger"
|
/// <returns>An HTTP handler with the <c>HX-Redirect</c> header set</returns>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-redirect/">Documentation</seealso>
|
||||||
|
let withHxRedirect (url: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Redirect" url
|
||||||
|
|
||||||
/// Allows you to trigger multiple client side events
|
/// <summary>If set to <c>true</c> the client side will do a full refresh of the page</summary>
|
||||||
let withHxTriggerMany evts : HttpHandler =
|
/// <param name="shouldRefresh">Whether the client should refresh their page</param>
|
||||||
toJson evts |> setHttpHeader "HX-Trigger"
|
/// <returns>An HTTP handler with the <c>HX-Refresh</c> header set</returns>
|
||||||
|
let withHxRefresh shouldRefresh : HttpHandler =
|
||||||
|
(toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh
|
||||||
|
|
||||||
/// Allows you to trigger a single client side event after changes have settled
|
/// <summary>Replaces the current URL in the history stack</summary>
|
||||||
let withHxTriggerAfterSettle : string -> HttpHandler =
|
/// <param name="url">The URL to place in the history stack in place of the current one</param>
|
||||||
setHttpHeader "HX-Trigger-After-Settle"
|
/// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set</returns>
|
||||||
|
/// <remarks>Use <see cref="withHxNoRelaceUrl" /> to explicitly not replace the current URL</remarks>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
|
||||||
|
let withHxReplaceUrl url : HttpHandler =
|
||||||
|
setHttpHeader "HX-Replace-Url" url
|
||||||
|
|
||||||
/// Allows you to trigger multiple client side events after changes have settled
|
/// <summary>Explicitly do not replace the current URL in the history stack</summary>
|
||||||
let withHxTriggerManyAfterSettle evts : HttpHandler =
|
/// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set to <c>false</c></returns>
|
||||||
toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
|
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
|
||||||
|
let withHxNoReplaceUrl : HttpHandler =
|
||||||
|
toLowerBool false |> withHxReplaceUrl
|
||||||
|
|
||||||
/// Allows you to trigger a single client side event after DOM swapping occurs
|
/// <summary>Override which portion of the response will be swapped into the target document</summary>
|
||||||
let withHxTriggerAfterSwap : string -> HttpHandler =
|
/// <param name="target">The selector for the new response target</param>
|
||||||
setHttpHeader "HX-Trigger-After-Swap"
|
/// <returns>An HTTP handler with the <c>HX-Reselect</c> header set</returns>
|
||||||
|
let withHxReselect (target: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Reselect" target
|
||||||
|
|
||||||
/// Allows you to trigger multiple client side events after DOM swapping occurs
|
/// <summary>Override the <c>hx-swap</c> attribute from the initiating element</summary>
|
||||||
let withHxTriggerManyAfterSwap evts : HttpHandler =
|
/// <param name="swap">The swap value to override</param>
|
||||||
toJson evts |> setHttpHeader "HX-Trigger-After-Swap"
|
/// <returns>An HTTP handler with the <c>HX-Reswap</c> header set</returns>
|
||||||
|
/// <remarks>Use <see cref="T:Giraffe.Htmx.Common.HxSwap">HxSwap</see> constants for best results</remarks>
|
||||||
|
let withHxReswap (swap: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Reswap" swap
|
||||||
|
|
||||||
|
/// <summary>Allows you to override the <c>hx-target</c> attribute</summary>
|
||||||
|
/// <param name="target">The new target for the response</param>
|
||||||
|
/// <returns>An HTTP handler with the <c>HX-Retarget</c> header set</returns>
|
||||||
|
let withHxRetarget (target: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Retarget" target
|
||||||
|
|
||||||
|
/// <summary>Allows you to trigger a single client side event</summary>
|
||||||
|
/// <param name="evt">The call to the event that should be triggered</param>
|
||||||
|
/// <returns>An HTTP handler with the <c>HX-Trigger</c> header set</returns>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||||
|
let withHxTrigger (evt: string) : HttpHandler =
|
||||||
|
setHttpHeader "HX-Trigger" evt
|
||||||
|
|
||||||
|
/// <summary>Allows you to trigger multiple client side events</summary>
|
||||||
|
/// <param name="evts">The calls to events that should be triggered</param>
|
||||||
|
/// <returns>An HTTP handler with the <c>HX-Trigger</c> header set for all given events</returns>
|
||||||
|
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||||
|
let withHxTriggerMany evts : HttpHandler =
|
||||||
|
toJson evts |> setHttpHeader "HX-Trigger"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Load the package-provided version of the htmx script</summary>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HtmxScript =
|
||||||
|
|
||||||
|
open Giraffe.Htmx.Common
|
||||||
|
open Microsoft.AspNetCore.Html
|
||||||
|
|
||||||
|
/// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary>
|
||||||
|
let local = HtmlString $"""<script src="{StaticAssetUrl.htmx}"></script>"""
|
||||||
|
|
||||||
|
/// <summary><c>script</c> tag to load the package-provided version of the htmax script</summary>
|
||||||
|
let localMax = HtmlString $"""<script src="{StaticAssetUrl.htmax}"></script>"""
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
## Giraffe.Htmx
|
||||||
|
|
||||||
|
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
|
||||||
|
|
||||||
|
**htmx version: 4.0.0**
|
||||||
|
|
||||||
|
_Upgrading from v2.x: the [migration guide](https://four.htmx.org/docs#migrating-from-htmx-2x-to-4x) lists changes for v4. For this package, the `HX-Trigger` and `HX-Trigger-Name` headers are marked obsolete. They are replaced by `HX-Source`, which provides the triggering tag name and `id` attribute. The `HX-Prompt` header has also been marked as obsolete, as the `hx-prompt` attribute which generated its content has been removed._
|
||||||
|
|
||||||
|
_Obsolete elements will be removed in the first production v4 release._
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install the package.
|
||||||
|
2. Prior to using the request header extension properties or the header-setting `HttpHandler`s, `open Giraffe.Htmx`.
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
To obtain a request header, using the `IHeaderDictionary` extension properties:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let myHandler : HttpHander =
|
||||||
|
fun next ctx ->
|
||||||
|
match ctx.Target with
|
||||||
|
| Some elt -> ... // do something with id of the target element
|
||||||
|
| None -> ... // no target element provided
|
||||||
|
```
|
||||||
|
|
||||||
|
To set a response header:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let myHandler : HttpHander =
|
||||||
|
fun next ctx ->
|
||||||
|
// some meaningful work
|
||||||
|
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.
|
||||||
|
|
||||||
|
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`; to load the htmax bundle, use `HtmxScript.localMax`.
|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
module Common
|
||||||
|
|
||||||
|
open Expecto
|
||||||
|
open Giraffe.Htmx
|
||||||
|
|
||||||
|
/// Test to ensure the version was updated
|
||||||
|
let version =
|
||||||
|
test "HtmxVersion is correct" {
|
||||||
|
Expect.equal HtmxVersion "4.0.0" "htmx version incorrect"
|
||||||
|
}
|
||||||
|
|
||||||
|
let staticAssetUrl =
|
||||||
|
testList "StaticAssetUrl" [
|
||||||
|
test "htmx is correct" {
|
||||||
|
Expect.equal
|
||||||
|
StaticAssetUrl.htmx
|
||||||
|
$"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
|
||||||
|
"Static htmx URL incorrect"
|
||||||
|
}
|
||||||
|
test "htmax is correct" {
|
||||||
|
Expect.equal
|
||||||
|
StaticAssetUrl.htmax
|
||||||
|
$"/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"
|
||||||
|
"Static htmx URL incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxSwap module
|
||||||
|
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 "InnerMorph is correct" {
|
||||||
|
Expect.equal HxSwap.InnerMorph "innerMorph" "Inner Morph swap value incorrect"
|
||||||
|
}
|
||||||
|
test "OuterMorph is correct" {
|
||||||
|
Expect.equal HxSwap.OuterMorph "outerMorph" "Outer Morph swap value incorrect"
|
||||||
|
}
|
||||||
|
test "OuterSync is correct" {
|
||||||
|
Expect.equal HxSwap.OuterSync "outerSync" "Outer Sync swap value incorrect"
|
||||||
|
}
|
||||||
|
test "TextContent is correct" {
|
||||||
|
Expect.equal HxSwap.TextContent "textContent" "Text Content swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Before is correct" {
|
||||||
|
Expect.equal HxSwap.Before "before" "Before swap value incorrect"
|
||||||
|
}
|
||||||
|
test "BeforeBegin is correct" {
|
||||||
|
Expect.equal HxSwap.BeforeBegin HxSwap.Before "Before Begin swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Prepend is correct" {
|
||||||
|
Expect.equal HxSwap.Prepend "prepend" "Prepend swap value incorrect"
|
||||||
|
}
|
||||||
|
test "AfterBegin is correct" {
|
||||||
|
Expect.equal HxSwap.AfterBegin HxSwap.Prepend "Prepend swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Append is correct" {
|
||||||
|
Expect.equal HxSwap.Append "append" "Append swap value incorrect"
|
||||||
|
}
|
||||||
|
test "BeforeEnd is correct" {
|
||||||
|
Expect.equal HxSwap.BeforeEnd HxSwap.Append "Before End swap value incorrect"
|
||||||
|
}
|
||||||
|
test "After is correct" {
|
||||||
|
Expect.equal HxSwap.After "after" "After swap value incorrect"
|
||||||
|
}
|
||||||
|
test "AfterEnd is correct" {
|
||||||
|
Expect.equal HxSwap.AfterEnd HxSwap.After "After End swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Delete is correct" {
|
||||||
|
Expect.equal HxSwap.Delete "delete" "Delete swap value incorrect"
|
||||||
|
}
|
||||||
|
test "None is correct" {
|
||||||
|
Expect.equal HxSwap.None "none" "None swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Upsert is correct" {
|
||||||
|
Expect.equal HxSwap.Upsert "upsert" "Upsert swap value incorrect"
|
||||||
|
}
|
||||||
|
test "Download is correct" {
|
||||||
|
Expect.equal HxSwap.Download "download" "Download swap value incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// All tests for this module
|
||||||
|
let allTests = testList "Htmx.Common" [ version; staticAssetUrl; swap ]
|
||||||
@@ -0,0 +1,445 @@
|
|||||||
|
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 "HxPreloaded" [
|
||||||
|
test "succeeds when the header is not present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxPreloaded "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and true" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Preloaded", "true")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxPreloaded "There should be a header present"
|
||||||
|
Expect.isTrue ctx.Request.Headers.HxPreloaded.Value "The header should have been true"
|
||||||
|
}
|
||||||
|
// This is not a condition fired by the extension; the header will be either absent or true
|
||||||
|
test "succeeds when the header is present and false" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Preloaded", "false")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxPreloaded "There should be a header present"
|
||||||
|
Expect.isFalse ctx.Request.Headers.HxPreloaded.Value "The header should have been false"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "HxPrompt" [
|
||||||
|
test "succeeds when the header is not present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Prompt", "of course")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present"
|
||||||
|
Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "HxRequest" [
|
||||||
|
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 "HxRequestId" [
|
||||||
|
test "succeeds when the header is not present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxRequestId "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Request-ID", "abcd1234")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxRequestId "There should be a header present"
|
||||||
|
Expect.equal ctx.Request.Headers.HxRequestId.Value "abcd1234" "The header value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "HxRequestType" [
|
||||||
|
test "succeeds when the header is not present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxRequestType "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is invalid" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Request-Type", "relaxed")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxRequestType "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds for a full request" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Request-Type", "full")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxRequestType "There have been a header returned"
|
||||||
|
Expect.equal ctx.Request.Headers.HxRequestType.Value HxFullRequest "The header value is incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for a partial request" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Request-Type", "partial")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
Expect.isSome ctx.Request.Headers.HxRequestType "There have been a header returned"
|
||||||
|
Expect.equal ctx.Request.Headers.HxRequestType.Value HxPartialRequest "The header value is incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "HxSource" [
|
||||||
|
test "succeeds when the header is not present" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||||
|
Expect.isNone ctx.Request.Headers.HxSource "There should not have been a header returned"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and both parts exist" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Source", "button#theId")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxSource
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "button" "The source tag was incorrect"
|
||||||
|
Expect.isSome (snd hdr.Value) "There should be a source ID present"
|
||||||
|
Expect.equal (snd hdr.Value).Value "theId" "The source ID was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and ID is blank" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Source", "a#")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxSource
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "a" "The source tag was incorrect"
|
||||||
|
Expect.isNone (snd hdr.Value) "There should not be a source ID present"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and ID is missing" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Source", "form")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxSource
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "form" "The source tag was incorrect"
|
||||||
|
Expect.isNone (snd hdr.Value) "There should not be a source ID present"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
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 and both parts exist" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Target", "div#leItem")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxTarget
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "div" "The target tag was incorrect"
|
||||||
|
Expect.isSome (snd hdr.Value) "There should be a target ID present"
|
||||||
|
Expect.equal (snd hdr.Value).Value "leItem" "The header value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and ID is blank" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Target", "span#")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxTarget
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "span" "The target tag was incorrect"
|
||||||
|
Expect.isNone (snd hdr.Value) "There should not be a target ID present"
|
||||||
|
}
|
||||||
|
test "succeeds when the header is present and ID is missing" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
dic.Add("HX-Target", "aside")
|
||||||
|
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let hdr = ctx.Request.Headers.HxTarget
|
||||||
|
Expect.isSome hdr "There should be a header present"
|
||||||
|
Expect.equal (fst hdr.Value) "aside" "The target tag was incorrect"
|
||||||
|
Expect.isNone (snd hdr.Value) "There should not be a target ID present"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// 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 "withHxDownload succeeds" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let! _ = withHxDownload "/files/stuff.pdf" next ctx
|
||||||
|
Expect.isTrue (dic.ContainsKey "HX-Download") "The HX-Download header should be present"
|
||||||
|
Expect.equal dic["HX-Download"].[0] "/files/stuff.pdf" "The HX-Download value was incorrect"
|
||||||
|
}
|
||||||
|
testTask "withHxLocation succeeds" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
let! _ = withHxLocation "/pagina-otro.html" next ctx
|
||||||
|
Expect.isTrue (dic.ContainsKey "HX-Location") "The HX-Location header should be present"
|
||||||
|
Expect.equal dic["HX-Location"].[0] "/pagina-otro.html" "The HX-Location value was incorrect"
|
||||||
|
}
|
||||||
|
testTask "withHxPushUrl succeeds" {
|
||||||
|
let ctx = Substitute.For<HttpContext>()
|
||||||
|
let dic = HeaderDictionary()
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HtmxScript module
|
||||||
|
let script =
|
||||||
|
testList "HtmxScript" [
|
||||||
|
test "local generates correct link" {
|
||||||
|
Expect.equal
|
||||||
|
(string HtmxScript.local)
|
||||||
|
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
|
||||||
|
"htmx script link is incorrect"
|
||||||
|
}
|
||||||
|
test "localMax generates correct link" {
|
||||||
|
Expect.equal
|
||||||
|
(string HtmxScript.localMax)
|
||||||
|
$"""<script src="/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"></script>"""
|
||||||
|
"htmx script link is incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// All tests for this module
|
||||||
|
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script ]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
open Expecto
|
||||||
|
|
||||||
|
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests; ViewEngineMax.allTests ]
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main args = runTestsWithCLIArgs [] args allTests
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<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="ViewEngineMax.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" />
|
||||||
|
<PackageReference Include="NSubstitute" />
|
||||||
|
<PackageReference Include="FSharp.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,880 @@
|
|||||||
|
module ViewEngine
|
||||||
|
|
||||||
|
open Expecto
|
||||||
|
open Giraffe.Htmx
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
open Giraffe.ViewEngine.Htmx
|
||||||
|
|
||||||
|
/// Tests for the HxConfig module
|
||||||
|
let hxConfig =
|
||||||
|
testList "HxConfig" [
|
||||||
|
testList "Configure" [
|
||||||
|
test "succeeds with an empty list" {
|
||||||
|
Expect.equal (HxConfig.Configure []) "{ }" "Configure with empty list incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds with a non-empty list" {
|
||||||
|
Expect.equal
|
||||||
|
(HxConfig.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
|
||||||
|
(HxConfig.Configure
|
||||||
|
[ HxConfig.Timeout 1000; HxConfig.Credentials false; HxConfig.NoHeaders true ])
|
||||||
|
"""{ "timeout": 1000, "credentials": false, "noHeaders": true }"""
|
||||||
|
"Configure with all known params incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
test "Timeout succeeds" {
|
||||||
|
Expect.equal (HxConfig.Timeout 50) "\"timeout\": 50" "Timeout value incorrect"
|
||||||
|
}
|
||||||
|
testList "Credentials" [
|
||||||
|
test "succeeds when set to true" {
|
||||||
|
Expect.equal (HxConfig.Credentials true) "\"credentials\": true" "Credentials value incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds when set to false" {
|
||||||
|
Expect.equal (HxConfig.Credentials false) "\"credentials\": false" "Credentials value incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "NoHeaders" [
|
||||||
|
test "succeeds when set to true" {
|
||||||
|
Expect.equal (HxConfig.NoHeaders true) "\"noHeaders\": true" "NoHeaders value incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds when set to false" {
|
||||||
|
Expect.equal (HxConfig.NoHeaders false) "\"noHeaders\": false" "NoHeaders value incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxEvent module
|
||||||
|
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 "AfterCleanup" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string AfterCleanup) "afterCleanup" "AfterCleanup event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (AfterCleanup.ToHxOnString()) "after:cleanup" "AfterCleanup hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterHistoryUpdate" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string AfterHistoryUpdate) "afterHistoryUpdate" "AfterHistoryUpdate event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterHistoryUpdate.ToHxOnString())
|
||||||
|
"after:history:update"
|
||||||
|
"AfterHistoryUpdate hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterInit" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string AfterInit) "afterInit" "AfterInit event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (AfterInit.ToHxOnString()) "after:init" "AfterInit hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterPushIntoHistory" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string AfterPushIntoHistory) "afterPushIntoHistory" "AfterPushIntoHistory event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterPushIntoHistory.ToHxOnString())
|
||||||
|
"after:push:into:history"
|
||||||
|
"AfterOnLoad hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterReplaceIntoHistory" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string AfterReplaceIntoHistory)
|
||||||
|
"afterReplaceIntoHistory"
|
||||||
|
"AfterReplaceIntoHistory event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterPushIntoHistory.ToHxOnString())
|
||||||
|
"after:push:into:history"
|
||||||
|
"AfterOnLoad 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 "BeforeCleanup" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeCleanup) "beforeCleanup" "BeforeCleanup event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeCleanup.ToHxOnString()) "before:cleanup" "BeforeCleanup hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeHistoryUpdate" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string BeforeHistoryUpdate) "beforeHistoryUpdate" "BeforeHistoryUpdate event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeHistoryUpdate.ToHxOnString())
|
||||||
|
"before:history:update"
|
||||||
|
"BeforeHistoryUpdate hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeInit" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeInit) "beforeInit" "BeforeInit event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (BeforeInit.ToHxOnString()) "before:init" "BeforeInit hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeProcess" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeProcess) "beforeProcess" "BeforeProcess event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeProcess.ToHxOnString()) "before:process" "BeforeProcess hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeRestoreHistory" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string BeforeRestoreHistory) "beforeRestoreHistory" "BeforeRestoreHistory event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeRestoreHistory.ToHxOnString())
|
||||||
|
"before:restore:history"
|
||||||
|
"BeforeRestoreHistory 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 "BeforeResponse" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeResponse) "beforeResponse" "BeforeResponse event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeResponse.ToHxOnString()) "before:response" "BeforeResponse 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 "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 "Error" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string Error) "error" "Error event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (Error.ToHxOnString()) "error" "Error hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "FinallyRequest" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string FinallyRequest) "finallyRequest" "FinallyRequest event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(FinallyRequest.ToHxOnString()) "finally:request" "FinallyRequest hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "ResponseError" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string ResponseError) "responseError" "ResponseError event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(ResponseError.ToHxOnString()) "response:error" "ResponseError hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxHeaders module
|
||||||
|
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 HxSync module
|
||||||
|
let hxSync =
|
||||||
|
testList "HxSync" [
|
||||||
|
test "Drop is correct" {
|
||||||
|
Expect.equal HxSync.Drop "drop" "Drop is incorrect"
|
||||||
|
}
|
||||||
|
test "Abort is correct" {
|
||||||
|
Expect.equal HxSync.Abort "abort" "Abort is incorrect"
|
||||||
|
}
|
||||||
|
test "Replace is correct" {
|
||||||
|
Expect.equal HxSync.Replace "replace" "Replace is incorrect"
|
||||||
|
}
|
||||||
|
test "Queue is correct" {
|
||||||
|
Expect.equal HxSync.Queue "queue" "Queue is incorrect"
|
||||||
|
}
|
||||||
|
test "QueueFirst is correct" {
|
||||||
|
Expect.equal HxSync.QueueFirst "queue first" "QueueFirst is incorrect"
|
||||||
|
}
|
||||||
|
test "QueueLast is correct" {
|
||||||
|
Expect.equal HxSync.QueueLast "queue last" "QueueLast is incorrect"
|
||||||
|
}
|
||||||
|
test "QueueAll is correct" {
|
||||||
|
Expect.equal HxSync.QueueAll "queue all" "QueueAll is incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxTrigger module
|
||||||
|
let hxTrigger =
|
||||||
|
testList "HxTrigger" [
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
test "succeeds when no type of queueing is given" {
|
||||||
|
Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "QueueFirst" [
|
||||||
|
test "succeeds when it is the first modifier" {
|
||||||
|
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 "_hxAction succeeds" {
|
||||||
|
form [ _hxAction "/endpoint/1" ] [] |> shouldRender """<form hx-action="/endpoint/1"></form>"""
|
||||||
|
}
|
||||||
|
test "_hxBoost succeeds" {
|
||||||
|
div [ _hxBoost ] [] |> shouldRender """<div hx-boost="true"></div>"""
|
||||||
|
}
|
||||||
|
test "_hxConfig succeeds" {
|
||||||
|
body [ _hxConfig [ ("test", "that") ] ] []
|
||||||
|
|> shouldRender """<body hx-config="{ "test": "that" }"></body>"""
|
||||||
|
}
|
||||||
|
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 "#that" ] [] |> shouldRender """<p hx-disable="#that"></p>"""
|
||||||
|
}
|
||||||
|
test "_hxEncoding succeeds" {
|
||||||
|
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
|
||||||
|
}
|
||||||
|
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="{ "X-Special-Header": "some-header" }"></figure>"""
|
||||||
|
}
|
||||||
|
test "_hxIgnore succeeds" {
|
||||||
|
span [ _hxIgnore ] [] |> shouldRender """<span hx-ignore></span>"""
|
||||||
|
}
|
||||||
|
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 "_hxMethod succeeds" {
|
||||||
|
div [ _hxMethod System.Net.Http.HttpMethod.Options ] []
|
||||||
|
|> shouldRender """<div hx-method="options"></div>"""
|
||||||
|
}
|
||||||
|
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:htmx:before:swap="changeStuff()"></strong>"""
|
||||||
|
}
|
||||||
|
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>"""
|
||||||
|
}
|
||||||
|
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 "_hxSelect succeeds" {
|
||||||
|
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
|
||||||
|
}
|
||||||
|
test "_hxSelectOob succeeds" {
|
||||||
|
section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
|
||||||
|
}
|
||||||
|
testList "_hxStatus" [
|
||||||
|
test "succeeds for a full code with no action" {
|
||||||
|
div [ _hxStatus "403" HxSwap.After "" ] [] |> shouldRender """<div hx-status:403="swap:after"></div>"""
|
||||||
|
}
|
||||||
|
test "succeeds for a two-digit range with action" {
|
||||||
|
span [ _hxStatus "41" HxSwap.Before "target:main" ] []
|
||||||
|
|> shouldRender """<span hx-status:41x="swap:before target:main"></span>"""
|
||||||
|
}
|
||||||
|
test "succeeds for a one-digit range with no action" {
|
||||||
|
p [ _hxStatus "5" HxSwap.None "" ] [] |> shouldRender """<p hx-status:5xx="swap:none"></p>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
test "_hxSwap succeeds" {
|
||||||
|
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" HxSync.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 "_hxValidate succeeds" {
|
||||||
|
form [ _hxValidate ] [] |> shouldRender """<form hx-validate></form>"""
|
||||||
|
}
|
||||||
|
test "_hxVals succeeds" {
|
||||||
|
dt [ _hxVals """{ "extra": "values" }""" ] []
|
||||||
|
|> shouldRender """<dt hx-vals="{ "extra": "values" }"></dt>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxModifiers module
|
||||||
|
let hxModifiers =
|
||||||
|
testList "HxModifiers" [
|
||||||
|
testList "hxAppend" [
|
||||||
|
test "succeeds with a value attribute" {
|
||||||
|
div [ hxAppend (_hxAction "/this/url") ] []
|
||||||
|
|> shouldRender """<div hx-action:append="/this/url"></div>"""
|
||||||
|
}
|
||||||
|
test "succeeds with a boolean attribute" {
|
||||||
|
form [ hxAppend _hxValidate ] []
|
||||||
|
|> shouldRender """<form hx-validate:append></form>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "hxInherited" [
|
||||||
|
test "succeeds with a value attribute" {
|
||||||
|
span [ hxInherited (_hxGet "/some/link") ] []
|
||||||
|
|> shouldRender """<span hx-get:inherited="/some/link"></span>"""
|
||||||
|
}
|
||||||
|
test "succeeds with a boolean attribute" {
|
||||||
|
p [ hxInherited _hxValidate ] []
|
||||||
|
|> shouldRender """<p hx-validate:inherited></p>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
test "succeeds when both are applied" {
|
||||||
|
div [ hxAppend (hxInherited (_hxPost "/there")) ] []
|
||||||
|
|> shouldRender """<div hx-post:inherited:append="/there"></div>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxTags module
|
||||||
|
let hxTags =
|
||||||
|
testList "HxTags" [
|
||||||
|
test "hxPartial succeeds" {
|
||||||
|
hxPartial [ _class "testy" ] [ str "Howdy" ]
|
||||||
|
|> shouldRender """<hx-partial class="testy">Howdy</hx-partial>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the Script module
|
||||||
|
let script =
|
||||||
|
testList "Script" [
|
||||||
|
test "local succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.local
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
|
||||||
|
"Local script tag is incorrect"
|
||||||
|
}
|
||||||
|
test "cdnMinified succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.cdnMinified
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-BvJpBiO8Kh31EqtJe5DRIeWrHWnCGkwytKs9NKFi86Hhw96dEqdEMzZDeK9iEGTc" crossorigin="anonymous"></script>"""
|
||||||
|
"CDN minified script tag is incorrect"
|
||||||
|
}
|
||||||
|
test "cdnUnminified succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.cdnUnminified
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-ESzWv77gBOAGtF3d7B8QiQ786cghRRsyhuYVVJGsrFGxwT0Dj1fLxReX9Ul6t4n9" crossorigin="anonymous"></script>"""
|
||||||
|
"CDN unminified script tag is incorrect"
|
||||||
|
}
|
||||||
|
testList "Max" [
|
||||||
|
test "localMax succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.Max.local
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"></script>"""
|
||||||
|
"Local script tag is incorrect"
|
||||||
|
}
|
||||||
|
test "cdnMaxMinified succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.Max.cdnMinified
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.min.js" integrity="sha384-F2+P/m0g8+he+KLpgsCVZlZGvOx2z83bU4rmmISSNt7br4A0RTcJ7ddMrz0v45Nv" crossorigin="anonymous"></script>"""
|
||||||
|
"CDN minified script tag is incorrect"
|
||||||
|
}
|
||||||
|
test "cdnMaxUnminified succeeds" {
|
||||||
|
let html = RenderView.AsString.htmlNode Script.Max.cdnUnminified
|
||||||
|
Expect.equal
|
||||||
|
html
|
||||||
|
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.js" integrity="sha384-wrTFbAj755gAdIPPR9n4aAVMkLWkzxjr1Zj+8D4kgr2App4h9doO6SXBH9wpivij" crossorigin="anonymous"></script>"""
|
||||||
|
"CDN 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>– ID {nodeId} not found –</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
|
||||||
|
hxSync
|
||||||
|
hxTrigger
|
||||||
|
hxVals
|
||||||
|
attributes
|
||||||
|
hxModifiers
|
||||||
|
hxTags
|
||||||
|
script
|
||||||
|
renderFragment
|
||||||
|
]
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
module ViewEngineMax
|
||||||
|
|
||||||
|
open Expecto
|
||||||
|
open Giraffe.ViewEngine.Htmax
|
||||||
|
|
||||||
|
/// Tests for the HxBrowserIndicatorConfigItem type
|
||||||
|
let hxBrowserIndicatorConfigItem =
|
||||||
|
testList "HxBrowserIndicatorConfigItem" [
|
||||||
|
testList "ToHcon" [
|
||||||
|
test "succeeds for BoostBrowserIndicator" {
|
||||||
|
Expect.equal
|
||||||
|
((BoostBrowserIndicator false).ToHcon())
|
||||||
|
"boostBrowserIndicator:false"
|
||||||
|
"The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxPreloadConfigItem type
|
||||||
|
let hxPreloadConfigItem =
|
||||||
|
testList "HxPreloadConfigItem" [
|
||||||
|
testList "ToHcon" [
|
||||||
|
test "succeeds for AutoBoost" {
|
||||||
|
Expect.equal ((AutoBoost false).ToHcon()) "autoBoost:false" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for BoostEvent" {
|
||||||
|
Expect.equal ((BoostEvent "mouseover").ToHcon()) "boostEvent:mouseover" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for BoostTimeout" {
|
||||||
|
Expect.equal ((BoostTimeout 30_000).ToHcon()) "boostTimeout:30000" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxSseWsConfigItem type
|
||||||
|
let hxSseWsConfigItem =
|
||||||
|
testList "HxSseWsConfigItem" [
|
||||||
|
testList "ToHcon" [
|
||||||
|
test "succeeds for Reconnect" {
|
||||||
|
Expect.equal ((Reconnect true).ToHcon()) "reconnect:true" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
testList "ReconnectDelay" [
|
||||||
|
test "succeeds for numeric value" {
|
||||||
|
Expect.equal ((ReconnectDelay "3000").ToHcon()) "reconnectDelay:3000" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for value with units" {
|
||||||
|
Expect.equal ((ReconnectDelay "3s").ToHcon()) "reconnectDelay:3s" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "ReconnectMaxDelay" [
|
||||||
|
test "succeeds for numeric value" {
|
||||||
|
Expect.equal
|
||||||
|
((ReconnectMaxDelay "30000").ToHcon()) "reconnectMaxDelay:30000" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for value with units" {
|
||||||
|
Expect.equal
|
||||||
|
((ReconnectMaxDelay "50s").ToHcon()) "reconnectMaxDelay:50s" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "ReconnectMaxAttempts" [
|
||||||
|
test "succeeds for negative 1" {
|
||||||
|
Expect.equal
|
||||||
|
((ReconnectMaxAttempts -1).ToHcon())
|
||||||
|
"reconnectMaxAttempts:Infinity"
|
||||||
|
"The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for zero" {
|
||||||
|
Expect.equal
|
||||||
|
((ReconnectMaxAttempts 0).ToHcon()) "reconnectMaxAttempts:0" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for positive number" {
|
||||||
|
Expect.equal
|
||||||
|
((ReconnectMaxAttempts 7).ToHcon()) "reconnectMaxAttempts:7" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
test "succeeds for ReconnectJitter" {
|
||||||
|
Expect.equal ((ReconnectJitter 0.7).ToHcon()) "reconnectJitter:0.7" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for PauseOnBackground" {
|
||||||
|
Expect.equal
|
||||||
|
((PauseOnBackground false).ToHcon()) "pauseOnBackground:false" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
test "succeeds for PendingRequestTtl" {
|
||||||
|
Expect.equal ((PendingRequestTtl 5000).ToHcon()) "pendingRequestTTL:5000" "The HCON value was incorrect"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxConfig module
|
||||||
|
let hxConfig =
|
||||||
|
testList "HxConfig" [
|
||||||
|
test "hxBrowserIndicatorConfig succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(hxBrowserIndicatorConfig [ BoostBrowserIndicator true ])
|
||||||
|
"browser-indicator.boostBrowserIndicator:true"
|
||||||
|
"Config settings not correct"
|
||||||
|
}
|
||||||
|
test "hxPreloadConfig succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(hxPreloadConfig [ AutoBoost true; BoostTimeout 3_000 ])
|
||||||
|
"preload.autoBoost:true preload.boostTimeout:3000"
|
||||||
|
"Config settings not correct"
|
||||||
|
}
|
||||||
|
test "hxSseConfig succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(hxSseConfig [ ReconnectDelay "5s"; PauseOnBackground false ])
|
||||||
|
"sse.reconnectDelay:5s sse.pauseOnBackground:false"
|
||||||
|
"Config settings not correct"
|
||||||
|
}
|
||||||
|
test "hxWsConfig succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(hxWsConfig [ Reconnect false; PendingRequestTtl 40_000 ])
|
||||||
|
"ws.reconnect:false ws.pendingRequestTTL:40000"
|
||||||
|
"Config settings not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
/// Tests for the HxEvent module
|
||||||
|
let hxEvent =
|
||||||
|
testList "HxEvent" [
|
||||||
|
testList "AfterSseConnection" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string AfterSseConnection) "afterSseConnection" "AfterSseConnection event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterSseConnection.ToHxOnString())
|
||||||
|
"after:sse:connection"
|
||||||
|
"AfterSseConnection hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterSseMessage" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterWsConnection" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string AfterWsConnection) "afterWsConnection" "AfterWsConnection event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterWsConnection.ToHxOnString())
|
||||||
|
"after:ws:connection"
|
||||||
|
"AfterWsConnection hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterWsMessage" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string AfterWsMessage) "afterWsMessage" "AfterWsMessage event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterWsMessage.ToHxOnString()) "after:ws:message" "AfterWsMessage hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "AfterWsRequest" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string AfterWsRequest) "afterWsRequest" "AfterWsRequest event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(AfterWsRequest.ToHxOnString()) "after:ws:request" "AfterWsRequest hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeSseConnection" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string BeforeSseConnection) "beforeSseConnection" "BeforeSseConnection event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeSseConnection.ToHxOnString())
|
||||||
|
"before:sse:connection"
|
||||||
|
"BeforeSseConnection hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeSseMessage" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeSseMessage.ToHxOnString())
|
||||||
|
"before:sse:message"
|
||||||
|
"BeforeSseMessage hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeWsConnection" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(string BeforeWsConnection) "beforeWsConnection" "BeforeWsConnection event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeWsConnection.ToHxOnString())
|
||||||
|
"before:ws:connection"
|
||||||
|
"BeforeWsConnection hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeWsMessage" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeWsMessage) "beforeWsMessage" "BeforeWsMessage event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeWsMessage.ToHxOnString()) "before:ws:message" "BeforeWsMessage hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "BeforeWsRequest" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string BeforeWsRequest) "beforeWsRequest" "BeforeWsRequest event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(BeforeWsRequest.ToHxOnString()) "before:ws:request" "BeforeWsRequest hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "DownloadComplete" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string DownloadComplete) "downloadComplete" "DownloadComplete event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(DownloadComplete.ToHxOnString())
|
||||||
|
"download:complete"
|
||||||
|
"DownloadComplete hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "DownloadProgress" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string DownloadProgress) "downloadProgress" "DownloadProgress event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(DownloadProgress.ToHxOnString())
|
||||||
|
"download:progress"
|
||||||
|
"DownloadProgress hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "DownloadStart" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string DownloadStart) "downloadStart" "DownloadStart event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal
|
||||||
|
(DownloadStart.ToHxOnString()) "download:start" "DownloadStart hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "SseClose" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string SseClose) "sseClose" "SseClose event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (SseClose.ToHxOnString()) "sse:close" "SseClose hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
testList "SseError" [
|
||||||
|
test "ToString succeeds" {
|
||||||
|
Expect.equal (string SseError) "sseError" "SseError event name not correct"
|
||||||
|
}
|
||||||
|
test "ToHxOnString succeeds" {
|
||||||
|
Expect.equal (SseError.ToHxOnString()) "sse:error" "SseError hx-on event name not correct"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
open Giraffe.ViewEngine
|
||||||
|
|
||||||
|
/// Pipe-able assertion for a rendered node
|
||||||
|
let shouldRender expected node =
|
||||||
|
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
|
||||||
|
|
||||||
|
/// Tests for the HtmaxAttrs module
|
||||||
|
let attributes =
|
||||||
|
testList "Attributes" [
|
||||||
|
|
||||||
|
test "_hxBrowserIndicator succeeds" {
|
||||||
|
a [ _hxBrowserIndicator ] [] |> shouldRender """<a hx-browser-indicator="true"></a>"""
|
||||||
|
}
|
||||||
|
test "_hxLive succeeds" {
|
||||||
|
main [ _hxLive "q().doStuff()" ] [] |> shouldRender """<main hx-live="q().doStuff()"></main>"""
|
||||||
|
}
|
||||||
|
test "_hxOnMax succeeds" {
|
||||||
|
body [ _hxOnMax SseClose "alert(done)" ] []
|
||||||
|
|> shouldRender """<body hx-on:htmx:sse:close="alert(done)"></body>"""
|
||||||
|
}
|
||||||
|
testList "_hxPreload" [
|
||||||
|
test "succeeds when given an event" {
|
||||||
|
blockquote [ _hxPreload (Some "focus") ] []
|
||||||
|
|> shouldRender """<blockquote hx-preload="focus"></blockquote>"""
|
||||||
|
}
|
||||||
|
test "succeeds when not given an event" {
|
||||||
|
em [ _hxPreload None ] [] |> shouldRender """<em hx-preload></em>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
test "_hxSseClose succeeds" {
|
||||||
|
form [ _hxSseClose "fin" ] [] |> shouldRender """<form hx-sse:close="fin"></form>"""
|
||||||
|
}
|
||||||
|
test "_hxSseConnect succeeds" {
|
||||||
|
div [ _hxSseConnect "/path/to/resource" ] []
|
||||||
|
|> shouldRender """<div hx-sse:connect="/path/to/resource"></div>"""
|
||||||
|
}
|
||||||
|
test "_hxTargets succeeds" {
|
||||||
|
dl [ _hxTargets ".ephemeral" ] [] |> shouldRender """<dl hx-targets=".ephemeral"></dl>"""
|
||||||
|
}
|
||||||
|
test "_hxWsConnect succeeds" {
|
||||||
|
p [ _hxWsConnect "/over/here" ] [] |> shouldRender """<p hx-ws:connect="/over/here"></p>"""
|
||||||
|
}
|
||||||
|
testList "_hxWsSend" [
|
||||||
|
test "succeeds when given a URL" {
|
||||||
|
span [ _hxWsSend (Some "/a/new/place") ] []
|
||||||
|
|> shouldRender """<span hx-ws:send="/a/new/place"></span>"""
|
||||||
|
}
|
||||||
|
test "succeeds when not given a URL" {
|
||||||
|
aside [ _hxWsSend None ] [] |> shouldRender """<aside hx-ws:send></aside>"""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let allTests =
|
||||||
|
testList "ViewEngine.Htmax" [
|
||||||
|
hxBrowserIndicatorConfigItem
|
||||||
|
hxPreloadConfigItem
|
||||||
|
hxSseWsConfigItem
|
||||||
|
hxConfig
|
||||||
|
hxEvent
|
||||||
|
attributes
|
||||||
|
]
|
||||||
@@ -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>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
module Program = let [<EntryPoint>] main _ = 0
|
|
||||||
@@ -1,245 +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 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 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 ``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)
|
|
||||||
|
|
||||||
|
|
||||||
/// 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="{ "X-Special-Header": "some-header" }"></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="{ "extra": "values" }"></dt>"""
|
|
||||||
|
|
||||||
[<Fact>]
|
|
||||||
let ``_hxWs succeeds`` () =
|
|
||||||
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""
|
|
||||||
|
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
<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>
|
||||||
|
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Htmx.fs" />
|
<Compile Include="Htmx.fs" />
|
||||||
|
<Compile Include="Htmax.fs" />
|
||||||
|
<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" />
|
||||||
|
<PackageReference Include="FSharp.Core" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Common\Giraffe.Htmx.Common.fsproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
/// <summary>Types and functions supporting htmax-bundled extension attributes in Giraffe View Engine</summary>
|
||||||
|
module Giraffe.ViewEngine.Htmax
|
||||||
|
|
||||||
|
/// <summary>Items which can be configured for the <c>hx-browser-indicator</c> extension</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#boosted-elements">Documentation</seealso>
|
||||||
|
type HxBrowserIndicatorConfigItem =
|
||||||
|
/// <summary>Whether to show the browser's native loading indicator for <c>hx-boost</c>ed links</summary>
|
||||||
|
| BoostBrowserIndicator of bool
|
||||||
|
|
||||||
|
/// <summary>Get the HCON representation of this configuration value</summary>
|
||||||
|
member this.ToHcon() =
|
||||||
|
match this with
|
||||||
|
| BoostBrowserIndicator it -> "boostBrowserIndicator:" + (string it).ToLowerInvariant()
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Items which can be configured for the <c>hx-preload</c> extension</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-preload#configuration">Documentation</seealso>
|
||||||
|
type HxPreloadConfigItem =
|
||||||
|
/// <summary>Whether items subject to <c>hx-boost</c> are automatically preloaded (default <c>true</c>)</summary>
|
||||||
|
| AutoBoost of bool
|
||||||
|
|
||||||
|
/// <summary>The event to use for <c>hx-boost</c> auto-preloaded content (default "mousedown")</summary>
|
||||||
|
| BoostEvent of string
|
||||||
|
|
||||||
|
/// <summary>The timeout (in ms) for <c>hx-boost</c> auto-preloaded requests (default 50_000)</summary>
|
||||||
|
| BoostTimeout of int
|
||||||
|
|
||||||
|
/// <summary>Get the HCON representation of this configuration value</summary>
|
||||||
|
member this.ToHcon() =
|
||||||
|
match this with
|
||||||
|
| AutoBoost it -> "autoBoost:" + (string it).ToLowerInvariant()
|
||||||
|
| BoostEvent it -> $"boostEvent:{it}"
|
||||||
|
| BoostTimeout it -> $"boostTimeout:{it}"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Items which can be configured for the <c>hx-sse</c> or <c>hx-ws</c> extensions</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-sse#configuration">SSE Documentation</seealso>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-ws#configuration">WebSockets Documentation</seealso>
|
||||||
|
type HxSseWsConfigItem =
|
||||||
|
/// <summary>Whether to automatically reconnect on stream end (default <c>true</c>)</summary>
|
||||||
|
| Reconnect of bool
|
||||||
|
|
||||||
|
/// <summary>Delay for reconnect attempts (ms, or numbers with units "1s", "2m"; default "500")</summary>
|
||||||
|
| ReconnectDelay of string
|
||||||
|
|
||||||
|
/// <summary>Maximum delay for reconnect attempts (ms, or numbers with units "1s, "2m"; default "60000")</summary>
|
||||||
|
| ReconnectMaxDelay of string
|
||||||
|
|
||||||
|
/// <summary>Reconnect maximum attempts (use -1 for Infinity; default Infinity)</summary>
|
||||||
|
| ReconnectMaxAttempts of int
|
||||||
|
|
||||||
|
/// <summary>Jitter to use when reconnecting (decimal between 0.0 and 1.0; default 0.3)</summary>
|
||||||
|
| ReconnectJitter of double
|
||||||
|
|
||||||
|
/// <summary>Whether to pause when the current tab is in the background (default <c>true</c>)</summary>
|
||||||
|
| PauseOnBackground of bool
|
||||||
|
|
||||||
|
/// <summary>Time-to-Live (TTL) for pending requests (<c>hx-ws</c> only)</summary>
|
||||||
|
| PendingRequestTtl of int
|
||||||
|
|
||||||
|
/// <summary>Get the HCON representation of this configuration value</summary>
|
||||||
|
member this.ToHcon() =
|
||||||
|
match this with
|
||||||
|
| Reconnect it -> "reconnect:" + (string it).ToLowerInvariant()
|
||||||
|
| ReconnectDelay it -> $"reconnectDelay:{it}"
|
||||||
|
| ReconnectMaxDelay it -> $"reconnectMaxDelay:{it}"
|
||||||
|
| ReconnectMaxAttempts it -> "reconnectMaxAttempts:" + if it = -1 then "Infinity" else string it
|
||||||
|
| ReconnectJitter it -> $"reconnectJitter:{it}"
|
||||||
|
| PauseOnBackground it -> "pauseOnBackground:" + (string it).ToLowerInvariant()
|
||||||
|
| PendingRequestTtl it -> $"pendingRequestTTL:{it}"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Helpers for generating extension configuration parameters</summary>
|
||||||
|
[<AutoOpen>]
|
||||||
|
module HxConfig =
|
||||||
|
|
||||||
|
/// Create an HCON section suitable for hx-config
|
||||||
|
let private hconSettings title values =
|
||||||
|
values |> Seq.map (sprintf "%s.%s" title) |> String.concat " "
|
||||||
|
|
||||||
|
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
|
||||||
|
/// <param name="items">The configuration items</param>
|
||||||
|
let hxBrowserIndicatorConfig (items: HxBrowserIndicatorConfigItem seq) =
|
||||||
|
items |> Seq.map _.ToHcon() |> hconSettings "browser-indicator"
|
||||||
|
|
||||||
|
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
|
||||||
|
/// <param name="items">The configuration items</param>
|
||||||
|
let hxPreloadConfig (items: HxPreloadConfigItem seq) =
|
||||||
|
items |> Seq.map _.ToHcon() |> hconSettings "preload"
|
||||||
|
|
||||||
|
/// <summary>Generate configuration items for SSE suitable for <c>hx-config</c></summary>
|
||||||
|
/// <param name="items">The configuration items</param>
|
||||||
|
let hxSseConfig (items: HxSseWsConfigItem seq) =
|
||||||
|
items |> Seq.map _.ToHcon() |> hconSettings "sse"
|
||||||
|
|
||||||
|
/// <summary>Generate configuration items for WebSockets suitable for <c>hx-config</c></summary>
|
||||||
|
/// <param name="items">The configuration items</param>
|
||||||
|
let hxWsConfig (items: HxSseWsConfigItem seq) =
|
||||||
|
items |> Seq.map _.ToHcon() |> hconSettings "ws"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>The events recognized by htmax-bundled extensions</summary>
|
||||||
|
[<Struct>]
|
||||||
|
type HxEvent =
|
||||||
|
|
||||||
|
/// <summary>Fired after an SSE connection is established</summary>
|
||||||
|
| AfterSseConnection
|
||||||
|
|
||||||
|
/// <summary>Fired after an SSE message is received</summary>
|
||||||
|
| AfterSseMessage
|
||||||
|
|
||||||
|
/// <summary>Fired after a WebSocket connection is established</summary>
|
||||||
|
| AfterWsConnection
|
||||||
|
|
||||||
|
/// <summary>Fired after a WebSocket message is received</summary>
|
||||||
|
| AfterWsMessage
|
||||||
|
|
||||||
|
/// <summary>Fired after a WebSocket request is sent</summary>
|
||||||
|
| AfterWsRequest
|
||||||
|
|
||||||
|
/// <summary>Fired before an SSE connection is established (cancelable)</summary>
|
||||||
|
| BeforeSseConnection
|
||||||
|
|
||||||
|
/// <summary>Fired before a received SSE message is processed (cancelable)</summary>
|
||||||
|
| BeforeSseMessage
|
||||||
|
|
||||||
|
/// <summary>Fired before a WebSocket connection is established (cancelable)</summary>
|
||||||
|
| BeforeWsConnection
|
||||||
|
|
||||||
|
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
|
||||||
|
| DownloadComplete
|
||||||
|
|
||||||
|
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
|
||||||
|
| DownloadProgress
|
||||||
|
|
||||||
|
/// <summary>Fired when a chunk is received for a download handled by <c>hx-download</c></summary>
|
||||||
|
| DownloadStart
|
||||||
|
|
||||||
|
/// <summary>Fired before a received WebSocket message is processed (cancelable)</summary>
|
||||||
|
| BeforeWsMessage
|
||||||
|
|
||||||
|
/// <summary>Fired before a WebSocket request is sent (cancelable)</summary>
|
||||||
|
| BeforeWsRequest
|
||||||
|
|
||||||
|
/// <summary>Fired when an SSE connection is closed</summary>
|
||||||
|
| SseClose
|
||||||
|
|
||||||
|
/// <summary>Fired when an SSE connection errors</summary>
|
||||||
|
| SseError
|
||||||
|
|
||||||
|
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
|
||||||
|
static member private Values = Map [
|
||||||
|
AfterSseConnection, ("afterSseConnection", "after:sse:connection")
|
||||||
|
AfterSseMessage, ("afterSseMessage", "after:sse:message")
|
||||||
|
AfterWsConnection, ("afterWsConnection", "after:ws:connection")
|
||||||
|
AfterWsMessage, ("afterWsMessage", "after:ws:message")
|
||||||
|
AfterWsRequest, ("afterWsRequest", "after:ws:request")
|
||||||
|
BeforeSseConnection, ("beforeSseConnection", "before:sse:connection")
|
||||||
|
BeforeSseMessage, ("beforeSseMessage", "before:sse:message")
|
||||||
|
BeforeWsConnection, ("beforeWsConnection", "before:ws:connection")
|
||||||
|
BeforeWsMessage, ("beforeWsMessage", "before:ws:message")
|
||||||
|
BeforeWsRequest, ("beforeWsRequest", "before:ws:request")
|
||||||
|
DownloadComplete, ("downloadComplete", "download:complete")
|
||||||
|
DownloadProgress, ("downloadProgress", "download:progress")
|
||||||
|
DownloadStart, ("downloadStart", "download:start")
|
||||||
|
SseClose, ("sseClose", "sse:close")
|
||||||
|
SseError, ("sseError", "sse:error")
|
||||||
|
]
|
||||||
|
|
||||||
|
/// <summary>The htmx event name</summary>
|
||||||
|
override this.ToString() = fst HxEvent.Values[this]
|
||||||
|
|
||||||
|
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary>
|
||||||
|
member this.ToHxOnString() = snd HxEvent.Values[this]
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Attributes and flags for htmax-bundled extensions</summary>
|
||||||
|
[<AutoOpen>]
|
||||||
|
module HtmaxAttrs =
|
||||||
|
|
||||||
|
/// <summary>Display the browser's native loading indicator when an htmx request is in flight</summary>
|
||||||
|
/// <returns>A configured <c>hx-browser-indicator</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#usage">Documentation</seealso>
|
||||||
|
let _hxBrowserIndicator =
|
||||||
|
attr "hx-browser-indicator" "true"
|
||||||
|
|
||||||
|
/// <summary>Run script each time the DOM changes</summary>
|
||||||
|
/// <param name="script">The script to be run each time the DOM changes (may use <c>q</c> helper)</param>
|
||||||
|
/// <returns>A configured <c>hx-live</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-live#the-hx-live-attribute">Documentation</seealso>
|
||||||
|
let _hxLive script =
|
||||||
|
attr "hx-live" script
|
||||||
|
|
||||||
|
/// <summary>Generate an <c>hx-on:htmx</c> event for an htmax-bundled extension event</summary>
|
||||||
|
/// <param name="event">The <c>HxEvent</c> to be handled</param>
|
||||||
|
/// <param name="handler">The script to be executed when the event occurs</param>
|
||||||
|
/// <returns>A configured <c>hx-on:htmx:</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
|
||||||
|
let _hxOnMax (event: HxEvent) handler =
|
||||||
|
Htmx.HtmxAttrs._hxOnEvent $"htmx:{event.ToHxOnString()}" handler
|
||||||
|
|
||||||
|
/// <summary>Identify a resource as one that should be preloaded</summary>
|
||||||
|
/// <param name="evt">The DOM event or htmx trigger which should cause the preload (optional; default behavior is
|
||||||
|
/// preloading on <c>mousedown</c>)</param>
|
||||||
|
/// <returns>A configured <c>hx-preload</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-preload#usage">Documentation</seealso>
|
||||||
|
let _hxPreload evt =
|
||||||
|
match evt with Some it -> attr "hx-preload" it | None -> flag "hx-preload"
|
||||||
|
|
||||||
|
/// <summary>Define an SSE message which indicates the connection should be closed</summary>
|
||||||
|
/// <param name="message">The text of the message which indicates the SSE connection should be closed</param>
|
||||||
|
/// <returns>A configured <c>hx-sse:close</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseclose">Documentation</seealso>
|
||||||
|
let _hxSseClose message =
|
||||||
|
attr "hx-sse:close" message
|
||||||
|
|
||||||
|
/// <summary>Define a URL to which an SSE connection should be established</summary>
|
||||||
|
/// <param name="url">The URL to which an SSE connection should be established</param>
|
||||||
|
/// <returns>A configured <c>hx-sse:connect</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseconnect">Documentation</seealso>
|
||||||
|
let _hxSseConnect url =
|
||||||
|
attr "hx-sse:connect" url
|
||||||
|
|
||||||
|
/// <summary>Replace multiple targets with the same content</summary>
|
||||||
|
/// <param name="selector">The CSS selector to identify targets for the replacement</param>
|
||||||
|
/// <returns>A configured <c>hx-targets</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-targets#usage">Documentation</seealso>
|
||||||
|
let _hxTargets selector =
|
||||||
|
attr "hx-targets" selector
|
||||||
|
|
||||||
|
/// <summary>Connect to a WebSocket URL</summary>
|
||||||
|
/// <param name="url">The URL to which a WebSocket connection should be established</param>
|
||||||
|
/// <returns>A configured <c>hx-ws:connect</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
|
||||||
|
let _hxWsConnect url =
|
||||||
|
attr "hx-ws:connect" url
|
||||||
|
|
||||||
|
/// <summary>Send data via a WebSocket connection</summary>
|
||||||
|
/// <param name="url">The URL to which the data should be sent (optional; default is connection URL)</param>
|
||||||
|
/// <returns>A configured <c>hx-ws:send</c> attribute</returns>
|
||||||
|
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
|
||||||
|
let _hxWsSend url =
|
||||||
|
match url with Some it -> attr "hx-ws:send" it | None -> flag "hx-ws:send"
|
||||||
+837
-121
@@ -1,139 +1,855 @@
|
|||||||
module Giraffe.ViewEngine.Htmx
|
/// <summary>Types and functions supporting htmx attributes in Giraffe View Engine</summary>
|
||||||
|
module Giraffe.ViewEngine.Htmx
|
||||||
|
|
||||||
/// Valid values for the `hx-encoding` attribute
|
/// <summary>Helpers to define <c>hx-config</c> attribute values</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/attributes/hx-config/">Documentation</seealso>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxConfig =
|
||||||
|
|
||||||
|
open Giraffe.Htmx.Common
|
||||||
|
|
||||||
|
/// <summary>Configure the request with various options</summary>
|
||||||
|
/// <param name="opts">The options to configure</param>
|
||||||
|
/// <returns>A string with the configured options</returns>
|
||||||
|
let Configure (opts: string list) =
|
||||||
|
opts
|
||||||
|
|> String.concat ", "
|
||||||
|
|> sprintf "{ %s }"
|
||||||
|
|
||||||
|
/// <summary>Set a timeout (in milliseconds)</summary>
|
||||||
|
/// <param name="ms">The milliseconds for the request timeout</param>
|
||||||
|
/// <returns>A string with the configured request timeout</returns>
|
||||||
|
let Timeout (ms: int) =
|
||||||
|
$"\"timeout\": {ms}"
|
||||||
|
|
||||||
|
/// <summary>Include or exclude credentials from the request</summary>
|
||||||
|
/// <param name="send"><c>true</c> if credentials should be sent, <c>false</c> if not</param>
|
||||||
|
/// <returns>A string with the configured credential options</returns>
|
||||||
|
let Credentials send =
|
||||||
|
(toLowerBool >> sprintf "\"credentials\": %s") send
|
||||||
|
|
||||||
|
/// <summary>Exclude or include headers from the request</summary>
|
||||||
|
/// <param name="exclude">
|
||||||
|
/// <c>true</c> if no headers should be sent; <c>false</c> if headers should be sent
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A string with the configured header options</returns>
|
||||||
|
let NoHeaders exclude =
|
||||||
|
(toLowerBool >> sprintf "\"noHeaders\": %s") exclude
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Valid values for the <c>hx-encoding</c> attribute</summary>
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxEncoding =
|
module HxEncoding =
|
||||||
/// A standard HTTP form
|
|
||||||
let Form = "application/x-www-form-urlencoded"
|
|
||||||
/// A multipart form (used for file uploads)
|
|
||||||
let MultipartForm = "multipart/form-data"
|
|
||||||
|
|
||||||
// TODO: hx-header helper
|
/// <summary>A standard HTTP form</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Form = "application/x-www-form-urlencoded"
|
||||||
|
|
||||||
/// Values / helpers for the `hx-params` attribute
|
/// <summary>A multipart form (used for file uploads)</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let MultipartForm = "multipart/form-data"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>The events recognized by htmx</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/events/">Documentation</seealso>
|
||||||
|
[<Struct>]
|
||||||
|
type HxEvent =
|
||||||
|
|
||||||
|
/// <summary>Send this event to an element to abort a request</summary>
|
||||||
|
| Abort
|
||||||
|
|
||||||
|
/// <summary>Triggered after htmx disables an element or removes it from the DOM</summary>
|
||||||
|
| AfterCleanup
|
||||||
|
|
||||||
|
/// <summary>Triggered after content is saved to the history cache</summary>
|
||||||
|
| AfterHistoryUpdate
|
||||||
|
|
||||||
|
/// <summary>Triggered after a request has initialized</summary>
|
||||||
|
| AfterInit
|
||||||
|
|
||||||
|
/// <summary>Triggered after htmx has initialized a DOM node or subtree</summary>
|
||||||
|
| AfterProcess
|
||||||
|
|
||||||
|
/// <summary>Triggered after new content is saved to the history cache</summary>
|
||||||
|
| AfterPushIntoHistory
|
||||||
|
|
||||||
|
/// <summary>Triggered after updated content is saved to the history cache</summary>
|
||||||
|
| AfterReplaceIntoHistory
|
||||||
|
|
||||||
|
/// <summary>Triggered after an HTTP request has completed successfully</summary>
|
||||||
|
| AfterRequest
|
||||||
|
|
||||||
|
/// <summary>Triggered after the DOM has settled</summary>
|
||||||
|
| AfterSettle
|
||||||
|
|
||||||
|
/// <summary>Triggered after new content has been swapped in</summary>
|
||||||
|
| AfterSwap
|
||||||
|
|
||||||
|
/// <summary>Triggered after a CSS view transition completes</summary>
|
||||||
|
| AfterViewTransition
|
||||||
|
|
||||||
|
/// <summary>Triggered before htmx disables an element or removes it from the DOM</summary>
|
||||||
|
| BeforeCleanup
|
||||||
|
|
||||||
|
/// <summary>Triggered before content is saved to the history cache</summary>
|
||||||
|
| BeforeHistoryUpdate
|
||||||
|
|
||||||
|
/// <summary>Triggered before htmx initializes a node</summary>
|
||||||
|
| BeforeInit
|
||||||
|
|
||||||
|
/// <summary>Triggered before htmx begins processing a DOM node or subtree</summary>
|
||||||
|
| BeforeProcess
|
||||||
|
|
||||||
|
/// <summary>Triggered before an HTTP request is made</summary>
|
||||||
|
| BeforeRequest
|
||||||
|
|
||||||
|
/// <summary>Triggered before an HTTP response is processed (cancelable)</summary>
|
||||||
|
| BeforeResponse
|
||||||
|
|
||||||
|
/// <summary>Triggered before a history restore request is made</summary>
|
||||||
|
| BeforeRestoreHistory
|
||||||
|
|
||||||
|
/// <summary>Triggered before a swap is done, allows you to configure the swap</summary>
|
||||||
|
| BeforeSwap
|
||||||
|
|
||||||
|
/// <summary>Triggered before a CSS view transition starts</summary>
|
||||||
|
| BeforeViewTransition
|
||||||
|
|
||||||
|
/// <summary>Triggered before the request, allows you to customize parameters, headers</summary>
|
||||||
|
| ConfigRequest
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request
|
||||||
|
/// </summary>
|
||||||
|
| Confirm
|
||||||
|
|
||||||
|
/// <summary>Triggered when an error occurs</summary>
|
||||||
|
| Error
|
||||||
|
|
||||||
|
/// <summary>Triggered after an HTTP request is made, whether it was successful or not</summary>
|
||||||
|
| FinallyRequest
|
||||||
|
|
||||||
|
/// <summary>Triggered when an HTTP response error (non-200 or 300 response code) occurs</summary>
|
||||||
|
| ResponseError
|
||||||
|
|
||||||
|
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
|
||||||
|
static member private Values = Map [
|
||||||
|
Abort, ("abort", "abort")
|
||||||
|
AfterCleanup, ("afterCleanup", "after:cleanup")
|
||||||
|
AfterHistoryUpdate, ("afterHistoryUpdate", "after:history:update")
|
||||||
|
AfterInit, ("afterInit", "after:init")
|
||||||
|
AfterPushIntoHistory, ("afterPushIntoHistory", "after:push:into:history")
|
||||||
|
AfterReplaceIntoHistory, ("afterReplaceIntoHistory", "after:replace:into:history")
|
||||||
|
AfterRequest, ("afterRequest", "after:request")
|
||||||
|
AfterSettle, ("afterSettle", "after:settle")
|
||||||
|
AfterSwap, ("afterSwap", "after:swap")
|
||||||
|
AfterViewTransition, ("afterViewTransition", "after:viewTransition")
|
||||||
|
BeforeCleanup, ("beforeCleanup", "before:cleanup")
|
||||||
|
BeforeHistoryUpdate, ("beforeHistoryUpdate", "before:history:update")
|
||||||
|
BeforeInit, ("beforeInit", "before:init")
|
||||||
|
BeforeProcess, ("beforeProcess", "before:process")
|
||||||
|
BeforeRequest, ("beforeRequest", "before:request")
|
||||||
|
BeforeResponse, ("beforeResponse", "before:response")
|
||||||
|
BeforeRestoreHistory, ("beforeRestoreHistory", "before:restore:history")
|
||||||
|
BeforeSwap, ("beforeSwap", "before:swap")
|
||||||
|
BeforeViewTransition, ("beforeTransition", "before:viewTransition")
|
||||||
|
ConfigRequest, ("configRequest", "config:request")
|
||||||
|
Confirm, ("confirm", "confirm")
|
||||||
|
Error, ("error", "error")
|
||||||
|
FinallyRequest, ("finallyRequest", "finally:request")
|
||||||
|
ResponseError, ("responseError", "response:error")
|
||||||
|
]
|
||||||
|
|
||||||
|
/// <summary>The htmx event name</summary>
|
||||||
|
override this.ToString() = fst HxEvent.Values[this]
|
||||||
|
|
||||||
|
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary>
|
||||||
|
member this.ToHxOnString() = snd HxEvent.Values[this]
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Helper to create the <c>hx-headers</c> attribute</summary>
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxParams =
|
module HxHeaders =
|
||||||
/// Include all parameters
|
|
||||||
let All = "*"
|
|
||||||
/// Include no parameters
|
|
||||||
let None = "none"
|
|
||||||
/// 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"
|
|
||||||
|
|
||||||
// TODO: hx-request helper
|
/// <summary>Create headers from a list of key/value pairs</summary>
|
||||||
|
let From = Giraffe.Htmx.Common.toJson
|
||||||
|
|
||||||
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config)
|
|
||||||
|
/// <summary>Helpers for the <c>hx-sync</c> attribute</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxSwap =
|
module HxSync =
|
||||||
/// 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"
|
|
||||||
|
|
||||||
/// Helpers for the `hx-trigger` attribute
|
/// <summary>Drop (ignore) this request if a request is already in flight</summary>
|
||||||
|
/// <remarks>This is the default for <c>hx-sync</c></remarks>
|
||||||
|
[<Literal>]
|
||||||
|
let Drop = "drop"
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drop (ignore) this request if a request is already in flight, and if another request occurs while this one is in
|
||||||
|
/// flight, abort this request
|
||||||
|
/// </summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Abort = "abort"
|
||||||
|
|
||||||
|
/// <summary>Abort any current in-flight request and replace it with this one</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Replace = "replace"
|
||||||
|
|
||||||
|
/// <summary>Place this request in an element-associated queue</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Queue = "queue"
|
||||||
|
|
||||||
|
/// <summary>Queue only the first request received while another request is in flight</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let QueueFirst = "queue first"
|
||||||
|
|
||||||
|
/// <summary>Queue only the last request received while another request is in flight</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let QueueLast = "queue last"
|
||||||
|
|
||||||
|
/// <summary>Queue all requests received while another request is in flight</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let QueueAll = "queue all"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Helpers for the <c>hx-trigger</c> attribute</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxTrigger =
|
module HxTrigger =
|
||||||
/// Append a filter to a trigger
|
|
||||||
let private appendFilter filter (trigger : string) =
|
|
||||||
match trigger.Contains "[" with
|
|
||||||
| true ->
|
|
||||||
let parts = trigger.Split ('[', ']')
|
|
||||||
$"{parts.[0]}[{parts.[1]}&&{filter}]"
|
|
||||||
| false -> $"{trigger}[{filter}]"
|
|
||||||
/// Trigger the event on a click
|
|
||||||
let Click = "click"
|
|
||||||
/// Trigger the event on page load
|
|
||||||
let Load = "load"
|
|
||||||
/// Helpers for defining filters
|
|
||||||
module Filter =
|
|
||||||
/// Only trigger the event if the `ALT` key is pressed
|
|
||||||
let Alt = appendFilter "altKey"
|
|
||||||
/// Only trigger the event if the `CTRL` key is pressed
|
|
||||||
let Ctrl = appendFilter "ctrlKey"
|
|
||||||
/// Only trigger the event if the `SHIFT` key is pressed
|
|
||||||
let Shift = appendFilter "shiftKey"
|
|
||||||
/// Only trigger the event if `CTRL+ALT` are pressed
|
|
||||||
let CtrlAlt = Ctrl >> Alt
|
|
||||||
/// Only trigger the event if `CTRL+SHIFT` are pressed
|
|
||||||
let CtrlShift = Ctrl >> Shift
|
|
||||||
/// Only trigger the event if `CTRL+ALT+SHIFT` are pressed
|
|
||||||
let CtrlAltShift = CtrlAlt >> Shift
|
|
||||||
/// Only trigger the event if `ALT+SHIFT` are pressed
|
|
||||||
let AltShift = Alt >> Shift
|
|
||||||
|
|
||||||
// TODO: more stuff for the hx-trigger helper
|
/// Append a filter to a trigger
|
||||||
|
let private appendFilter filter (trigger : string) =
|
||||||
|
match trigger.Contains "[" with
|
||||||
|
| true ->
|
||||||
|
let parts = trigger.Split ('[', ']')
|
||||||
|
$"{parts[0]}[{parts[1]}&&{filter}]"
|
||||||
|
| false -> $"{trigger}[{filter}]"
|
||||||
|
|
||||||
// TODO: hx-vals helper
|
/// <summary>Trigger the event on a click</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Click = "click"
|
||||||
|
|
||||||
|
/// <summary>Trigger the event on page load</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Load = "load"
|
||||||
|
|
||||||
|
/// <summary>Trigger the event when the item is visible</summary>
|
||||||
|
[<Literal>]
|
||||||
|
let Revealed = "revealed"
|
||||||
|
|
||||||
|
/// <summary>Trigger this event every [timing declaration]</summary>
|
||||||
|
/// <param name="duration">The duration on which this trigger should fire (e.g., "1s", "500ms")</param>
|
||||||
|
/// <returns>A trigger timing specification</returns>
|
||||||
|
let Every duration =
|
||||||
|
$"every %s{duration}"
|
||||||
|
|
||||||
|
/// <summary>Helpers for defining filters</summary>
|
||||||
|
module Filter =
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if the <c>ALT</c> key is pressed</summary>
|
||||||
|
let Alt = appendFilter "altKey"
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if the <c>CTRL</c> key is pressed</summary>
|
||||||
|
let Ctrl = appendFilter "ctrlKey"
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if the <c>SHIFT</c> key is pressed</summary>
|
||||||
|
let Shift = appendFilter "shiftKey"
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if <c>CTRL</c> and <c>ALT</c> are pressed</summary>
|
||||||
|
let CtrlAlt = Ctrl >> Alt
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if <c>CTRL</c> and <c>SHIFT</c> are pressed</summary>
|
||||||
|
let CtrlShift = Ctrl >> Shift
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if <c>CTRL</c>, <c>ALT</c>, and <c>SHIFT</c> are pressed</summary>
|
||||||
|
let CtrlAltShift = CtrlAlt >> Shift
|
||||||
|
|
||||||
|
/// <summary>Only trigger the event if <c>ALT</c> and <c>SHIFT</c> are pressed</summary>
|
||||||
|
let AltShift = Alt >> Shift
|
||||||
|
|
||||||
|
/// Append a modifier to the current trigger
|
||||||
|
let private appendModifier modifier current =
|
||||||
|
if current = "" then modifier else $"{current} {modifier}"
|
||||||
|
|
||||||
|
/// <summary>Only trigger once</summary>
|
||||||
|
/// <param name="action">The action which should only be fired once</param>
|
||||||
|
/// <returns>A trigger spec to fire the given action once</returns>
|
||||||
|
let Once action =
|
||||||
|
appendModifier "once" action
|
||||||
|
|
||||||
|
/// <summary>Trigger when changed</summary>
|
||||||
|
/// <param name="elt">The element from which the <c>onchange</c> event will be emitted</param>
|
||||||
|
/// <returns>A trigger spec to fire when the given element changes</returns>
|
||||||
|
let Changed elt =
|
||||||
|
appendModifier "changed" elt
|
||||||
|
|
||||||
|
/// <summary>Delay execution; resets every time the event is seen</summary>
|
||||||
|
/// <param name="duration">The duration for the delay (e.g., "1s", "500ms")</param>
|
||||||
|
/// <param name="action">The action which should be fired after the given delay</param>
|
||||||
|
/// <returns>A trigger spec to fire the given action after the specified delay</returns>
|
||||||
|
let Delay duration action =
|
||||||
|
appendModifier $"delay:%s{duration}" action
|
||||||
|
|
||||||
|
/// <summary>Throttle execution; ignore other events, fire when duration passes</summary>
|
||||||
|
/// <param name="duration">The duration for the throttling (e.g., "1s", "500ms")</param>
|
||||||
|
/// <param name="action">The action which should be fired after the given duration</param>
|
||||||
|
/// <returns>A trigger spec to fire the given action after the specified duration</returns>
|
||||||
|
let Throttle duration action =
|
||||||
|
appendModifier $"throttle:%s{duration}" action
|
||||||
|
|
||||||
|
/// <summary>Trigger this event from a CSS selector</summary>
|
||||||
|
/// <param name="selector">A CSS selector to identify elements which may fire this trigger</param>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire from the given element(s)</returns>
|
||||||
|
let From selector action =
|
||||||
|
appendModifier $"from:%s{selector}" action
|
||||||
|
|
||||||
|
/// <summary>Trigger this event from the <c>document</c> object</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire from the <c>document</c> element</returns>
|
||||||
|
let FromDocument action =
|
||||||
|
From "document" action
|
||||||
|
|
||||||
|
/// <summary>Trigger this event from the <c>window</c> object</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire from the <c>window</c> object</returns>
|
||||||
|
let FromWindow action =
|
||||||
|
From "window" action
|
||||||
|
|
||||||
|
/// <summary>Trigger this event from the closest parent CSS selector</summary>
|
||||||
|
/// <param name="selector">The CSS selector from which the action should be fired</param>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire from the closest element</returns>
|
||||||
|
let FromClosest selector action =
|
||||||
|
From $"closest %s{selector}" action
|
||||||
|
|
||||||
|
/// <summary>Trigger this event from the closest child CSS selector</summary>
|
||||||
|
/// <param name="selector">The child CSS selector from which the action should be fired</param>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire from the closest child element</returns>
|
||||||
|
let FromFind selector action =
|
||||||
|
From $"find %s{selector}" action
|
||||||
|
|
||||||
|
/// <summary>Target the given CSS selector with the results of this event</summary>
|
||||||
|
/// <param name="selector">The CSS selector to which the result of the action will be applied</param>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to target the given selector</returns>
|
||||||
|
let Target selector action =
|
||||||
|
appendModifier $"target:%s{selector}" action
|
||||||
|
|
||||||
|
/// <summary>Prevent any further events from occurring after this one fires</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to fire the given action and prevent further events</returns>
|
||||||
|
let Consume action =
|
||||||
|
appendModifier "consume" action
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configure queueing when events fire when others are in flight; if unspecified, the default is <c>last</c>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="how">
|
||||||
|
/// How the request should be queued (consider <see cref="QueueFirst" />, <see cref="QueueLast" />,
|
||||||
|
/// <see cref="QueueAll" />, and <see cref="QueueNone" />)
|
||||||
|
/// </param>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to queue the given action</returns>
|
||||||
|
let Queue how action =
|
||||||
|
let qSpec = if how = "" then "" else $":{how}"
|
||||||
|
appendModifier $"queue{qSpec}" action
|
||||||
|
|
||||||
|
/// <summary>Queue the first event, discard all others (i.e., a FIFO queue of 1)</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to queue the given action</returns>
|
||||||
|
let QueueFirst action =
|
||||||
|
Queue "first" action
|
||||||
|
|
||||||
|
/// <summary>Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to queue the given action</returns>
|
||||||
|
let QueueLast action =
|
||||||
|
Queue "last" action
|
||||||
|
|
||||||
|
/// <summary>Queue all events; discard none</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to queue the given action</returns>
|
||||||
|
let QueueAll action =
|
||||||
|
Queue "all" action
|
||||||
|
|
||||||
|
/// <summary>Queue no events; discard all</summary>
|
||||||
|
/// <param name="action">The action to be fired</param>
|
||||||
|
/// <returns>A trigger spec to queue the given action</returns>
|
||||||
|
let QueueNone action =
|
||||||
|
Queue "none" action
|
||||||
|
|
||||||
|
|
||||||
/// Attributes and flags for HTMX
|
/// <summary>Helper to create the <c>hx-vals</c> attribute</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxVals =
|
||||||
|
|
||||||
|
/// <summary>Create values from a list of key/value pairs</summary>
|
||||||
|
let From = Giraffe.Htmx.Common.toJson
|
||||||
|
|
||||||
|
|
||||||
|
open System.Net.Http
|
||||||
|
open Giraffe.Htmx
|
||||||
|
|
||||||
|
/// <summary>Attributes and flags for htmx</summary>
|
||||||
[<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"
|
/// <summary>The URL which will receive the request (use with <c>_hxMethod</c>)</summary>
|
||||||
/// Shows a confirm() dialog before issuing a request
|
/// <param name="url">The URL for the attribute</param>
|
||||||
let _hxConfirm = attr "hx-confirm"
|
/// <seealso href="https://four.htmx.org/attributes/hx-action/">Documentation</seealso>
|
||||||
/// Issues a DELETE to the specified URL
|
let _hxAction url =
|
||||||
let _hxDelete = attr "hx-delete"
|
attr "hx-action" url
|
||||||
/// Disables htmx processing for the given node and any children nodes
|
|
||||||
let _hxDisable = flag "hx-disable"
|
/// <summary>Progressively enhances anchors and forms to use AJAX requests</summary>
|
||||||
/// Changes the request encoding type
|
/// <remarks>Use <c>_hxNoBoost</c> to set to false</remarks>
|
||||||
let _hxEncoding = attr "hx-encoding"
|
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
|
||||||
/// Extensions to use for this element
|
let _hxBoost =
|
||||||
let _hxExt = attr "hx-ext"
|
attr "hx-boost" "true"
|
||||||
/// Issues a GET to the specified URL
|
|
||||||
let _hxGet = attr "hx-get"
|
/// <summary>Configure request behavior</summary>
|
||||||
/// Adds to the headers that will be submitted with the request
|
/// <param name="config">The configuration parameters to use for the request</param>
|
||||||
let _hxHeaders = attr "hx-headers"
|
/// <returns>A configured <c>hx-config</c> attribute</returns>
|
||||||
/// The element to snapshot and restore during history navigation
|
/// <seealso cref="https://four.htmx.org/attributes/hx-config/">Documentation</seealso>
|
||||||
let _hxHistoryElt = flag "hx-history-elt"
|
let _hxConfig config =
|
||||||
/// Includes additional data in AJAX requests
|
attr "hx-config" (toJson config)
|
||||||
let _hxInclude = attr "hx-include"
|
|
||||||
/// The element to put the htmx-request class on during the AJAX request
|
/// <summary>Shows a <c>confirm()</c> dialog before issuing a request</summary>
|
||||||
let _hxIndicator = attr "hx-indicator"
|
/// <param name="prompt">The prompt to present to the user when seeking their confirmation</param>
|
||||||
/// Overrides a previous `hx-boost`
|
/// <returns>A configured <c>hx-confirm</c> attribute</returns>
|
||||||
let _hxNoBoost = attr "hx-boost" "false"
|
/// <seealso href="https://htmx.org/attributes/hx-confirm/">Documentation</seealso>
|
||||||
/// Filters the parameters that will be submitted with a request
|
let _hxConfirm prompt =
|
||||||
let _hxParams = attr "hx-params"
|
attr "hx-confirm" prompt
|
||||||
/// Issues a PATCH to the specified URL
|
|
||||||
let _hxPatch = attr "hx-patch"
|
/// <summary>Issues a <c>DELETE</c> to the specified URL</summary>
|
||||||
/// Issues a POST to the specified URL
|
/// <param name="url">The URL to which the <c>DELETE</c> request should be sent</param>
|
||||||
let _hxPost = attr "hx-post"
|
/// <returns>A configured <c>hx-delete</c> attribute</returns>
|
||||||
/// Preserves an element between requests
|
/// <seealso href="https://htmx.org/attributes/hx-delete/">Documentation</seealso>
|
||||||
let _hxPreserve = attr "hx-preserve" "true"
|
let _hxDelete url =
|
||||||
/// Shows a prompt before submitting a request
|
attr "hx-delete" url
|
||||||
let _hxPrompt = attr "hx-prompt"
|
|
||||||
/// Pushes the URL into the location bar, creating a new history entry
|
/// <summary>Specifies elements that should be disabled when an htmx request is in flight</summary>
|
||||||
let _hxPushUrl = flag "hx-push-url"
|
/// <param name="elt">The element to disable when an htmx request is in flight</param>
|
||||||
/// Issues a PUT to the specified URL
|
/// <returns>A configured <c>hx-disable</c> attribute</returns>
|
||||||
let _hxPut = attr "hx-put"
|
/// <remarks>This behavior changed in v4
|
||||||
/// Configures various aspects of the request
|
/// <ul><li>For the v2 behavior of <c>hx-disable</c> (which will manifest as compile errors), change instances to
|
||||||
let _hxRequest = attr "hx-request"
|
/// <c>_hxIgnore</c></li>
|
||||||
/// Selects a subset of the server response to process
|
/// <li>For v4 behavior, changing <c>_hxDisabledElt</c> to <c>_hxDisable</c> should be sufficient</li></ul>
|
||||||
let _hxSelect = attr "hx-select"
|
/// </remarks>
|
||||||
/// Establishes and listens to Server Sent Event (SSE) sources for events
|
/// <seealso href="https://four.htmx.org/attributes/hx-disable/">Documentation</seealso>
|
||||||
let _hxSse = attr "hx-sse"
|
let _hxDisable elt =
|
||||||
/// Controls how the response content is swapped into the DOM (e.g. 'outerHTML' or 'beforeEnd')
|
attr "hx-disable" elt
|
||||||
let _hxSwap = attr "hx-swap"
|
|
||||||
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
|
/// <summary>Changes the request encoding type</summary>
|
||||||
let _hxSwapOob = attr "hx-swap-oob"
|
/// <param name="enc">The encoding type (use <c>HxEncoding</c> constants)</param>
|
||||||
/// Specifies the target element to be swapped
|
/// <returns>A configured <c>hx-encoding</c> attribute</returns>
|
||||||
let _hxTarget = attr "hx-target"
|
/// <seealso cref="HxEncoding" />
|
||||||
/// Specifies the event that triggers the request
|
/// <seealso href="https://htmx.org/attributes/hx-encoding/">Documentation</seealso>
|
||||||
let _hxTrigger = attr "hx-trigger"
|
let _hxEncoding enc =
|
||||||
/// Adds to the parameters that will be submitted with the request
|
attr "hx-encoding" enc
|
||||||
let _hxVals = attr "hx-vals"
|
|
||||||
/// Establishes a WebSocket or sends information to one
|
/// <summary>Issues a <c>GET</c> to the specified URL</summary>
|
||||||
let _hxWs = attr "hx-ws"
|
/// <param name="url">The URL to which the <c>GET</c> request should be sent</param>
|
||||||
|
/// <returns>A configured <c>hx-get</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-get/">Documentation</seealso>
|
||||||
|
let _hxGet url =
|
||||||
|
attr "hx-get" url
|
||||||
|
|
||||||
|
/// <summary>Adds to the headers that will be submitted with the request</summary>
|
||||||
|
/// <param name="hdrs">The headers to include with the request</param>
|
||||||
|
/// <returns>A configured <c>hx-headers</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-headers/">Documentation</seealso>
|
||||||
|
let _hxHeaders hdrs =
|
||||||
|
attr "hx-headers" hdrs
|
||||||
|
|
||||||
|
/// <summary>Disables htmx processing for the given node and any children nodes</summary>
|
||||||
|
/// <seealso href="https://four.htmx.org/attributes/hx-ignore/">Documentation</seealso>
|
||||||
|
let _hxIgnore =
|
||||||
|
flag "hx-ignore"
|
||||||
|
|
||||||
|
/// <summary>Includes additional data in AJAX requests</summary>
|
||||||
|
/// <param name="spec">The specification of what should be included in the request</param>
|
||||||
|
/// <returns>A configured <c>hx-include</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-include/">Documentation</seealso>
|
||||||
|
let _hxInclude spec =
|
||||||
|
attr "hx-include" spec
|
||||||
|
|
||||||
|
/// <summary>The element to put the <c>htmx-request</c> class on during the AJAX request</summary>
|
||||||
|
/// <param name="selector">The selector for the indicator element</param>
|
||||||
|
/// <returns>A configured <c>hx-indicator</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-indicator/">Documentation</seealso>
|
||||||
|
let _hxIndicator selector =
|
||||||
|
attr "hx-indicator" selector
|
||||||
|
|
||||||
|
/// <summary>The HTTP method to use for the request(use with <c>_hxAction</c>)</summary>
|
||||||
|
/// <param name="method">The method to use</param>
|
||||||
|
/// <seealso href="https://four.htmx.org/attributes/hx-method/">Documentation</seealso>
|
||||||
|
let _hxMethod (method: HttpMethod) =
|
||||||
|
attr "hx-method" (method.Method.ToLowerInvariant())
|
||||||
|
|
||||||
|
/// <summary>Overrides a previous <c>hx-boost</c> (hx-boost="false")</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
|
||||||
|
let _hxNoBoost =
|
||||||
|
attr "hx-boost" "false"
|
||||||
|
|
||||||
|
/// <summary>Attach an event handler for DOM events</summary>
|
||||||
|
/// <param name="evtName">The name of the event</param>
|
||||||
|
/// <param name="handler">The script to be executed when the event occurs</param>
|
||||||
|
/// <returns>A configured <c>hx-on</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
|
||||||
|
let _hxOnEvent evtName handler =
|
||||||
|
attr $"hx-on:%s{evtName}" handler
|
||||||
|
|
||||||
|
/// <summary>Attach an event handler for htmx events</summary>
|
||||||
|
/// <param name="hxEvent">The <c>HxEvent</c> to be handled</param>
|
||||||
|
/// <param name="handler">The script to be executed when the event occurs</param>
|
||||||
|
/// <returns>A configured <c>hx-on:htmx:</c> attribute</returns>
|
||||||
|
/// <remarks>For events defined in htmax, use <c>_hxOnMax</c></remarks>
|
||||||
|
/// <seealso cref="HxEvent" />
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
|
||||||
|
let _hxOnHxEvent (hxEvent: HxEvent) handler =
|
||||||
|
_hxOnEvent $"htmx:{hxEvent.ToHxOnString()}" handler
|
||||||
|
|
||||||
|
/// <summary>Issues a <c>PATCH</c> to the specified URL</summary>
|
||||||
|
/// <param name="url">The URL to which the request should be directed</param>
|
||||||
|
/// <returns>A configured <c>hx-patch</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-patch/">Documentation</seealso>
|
||||||
|
let _hxPatch url =
|
||||||
|
attr "hx-patch" url
|
||||||
|
|
||||||
|
/// <summary>Issues a <c>POST</c> to the specified URL</summary>
|
||||||
|
/// <param name="url">The URL to which the request should be directed</param>
|
||||||
|
/// <returns>A configured <c>hx-post</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-post/">Documentation</seealso>
|
||||||
|
let _hxPost url =
|
||||||
|
attr "hx-post" url
|
||||||
|
|
||||||
|
/// <summary>Preserves an element between requests</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-preserve/">Documentation</seealso>
|
||||||
|
let _hxPreserve =
|
||||||
|
flag "hx-preserve"
|
||||||
|
|
||||||
|
/// <summary>Shows a <c>prompt()</c> dialog before submitting a request</summary>
|
||||||
|
/// <param name="text">The text for the prompt</param>
|
||||||
|
/// <returns>A configured <c>hx-prompt</c> attribute</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// <ul>
|
||||||
|
/// <li><b>NEW IN v4:</b> This functionality is dependent on the hx-prompt extension being loaded</li>
|
||||||
|
/// <li>The value provided will be in the <c>HX-Prompt</c> request header</li>
|
||||||
|
/// </ul>
|
||||||
|
/// </remarks>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso>
|
||||||
|
let _hxPrompt text =
|
||||||
|
attr "hx-prompt" text
|
||||||
|
|
||||||
|
/// <summary>Pushes the URL into the location bar, creating a new history entry</summary>
|
||||||
|
/// <param name="spec">
|
||||||
|
/// <ul>
|
||||||
|
/// <li>"true" to push the fetched URL</li>
|
||||||
|
/// <li>"false" to explicitly not push the fetched URL</li>
|
||||||
|
/// <li>A specific URL to push</li>
|
||||||
|
/// </ul>
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A configured <c>hx-push-url</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-push-url/">Documentation</seealso>
|
||||||
|
let _hxPushUrl spec =
|
||||||
|
attr "hx-push-url" spec
|
||||||
|
|
||||||
|
/// <summary>Issues a <c>PUT</c> to the specified URL</summary>
|
||||||
|
/// <param name="url">The URL to which the request should be directed</param>
|
||||||
|
/// <returns>A configured <c>hx-put</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-put/">Documentation</seealso>
|
||||||
|
let _hxPut url =
|
||||||
|
attr "hx-put" url
|
||||||
|
|
||||||
|
/// <summary>Replaces the current URL in the browser's history stack</summary>
|
||||||
|
/// <param name="spec">
|
||||||
|
/// <ul>
|
||||||
|
/// <li>"true" to replace the current URL with the fetched one</li>
|
||||||
|
/// <li>"false" to explicitly replace nothing</li>
|
||||||
|
/// <li>A specific URL to replace in the browser's history</li>
|
||||||
|
/// </ul>
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A configured <c>hx-replace-url</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-replace-url/">Documentation</seealso>
|
||||||
|
let _hxReplaceUrl spec =
|
||||||
|
attr "hx-replace-url" spec
|
||||||
|
|
||||||
|
/// <summary>Selects a subset of the server response to process</summary>
|
||||||
|
/// <param name="selector">A CSS selector for the content to be selected</param>
|
||||||
|
/// <returns>A configured <c>hx-select</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-select/">Documentation</seealso>
|
||||||
|
let _hxSelect selector =
|
||||||
|
attr "hx-select" selector
|
||||||
|
|
||||||
|
/// <summary>Selects a subset of an out-of-band server response</summary>
|
||||||
|
/// <param name="selectors">One or more comma-delimited CSS selectors for the content to be selected</param>
|
||||||
|
/// <returns>A configured <c>hx-select-oob</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-select-oob/">Documentation</seealso>
|
||||||
|
let _hxSelectOob selectors =
|
||||||
|
attr "hx-select-oob" selectors
|
||||||
|
|
||||||
|
/// <summary>Define a response to an HTTP status code or code range</summary>
|
||||||
|
/// <param name="code">
|
||||||
|
/// The code or code range; for a code, pass the full code as a string (ex. <c>404</c>), and for a range, pass a
|
||||||
|
/// string with one or two characters (ex. <c>5</c> for all 500s, <c>42</c> for 420-429)
|
||||||
|
/// </param>
|
||||||
|
/// <param name="swap">The swap style to use for this action(use <c>HxSwap</c> values)</param>
|
||||||
|
/// <param name="action">
|
||||||
|
/// The action which should be taken for responses matching the status code; can be a combination of the following,
|
||||||
|
/// with each item separated by a space:
|
||||||
|
/// <ul>
|
||||||
|
/// <li><c>target:</c> and a CSS selector for the swap target (overrides original <c>hx-target</c>)</li>
|
||||||
|
/// <li><c>select:</c> and a CSS selector for the content to be swapped (overrides original <c>hx-select</c>)</li>
|
||||||
|
/// <li><c>push:</c> and <c>true</c>, <c>false</c>, or a URL; <c>false</c> does not update URL history, <c>true</c>
|
||||||
|
/// updates with the current URL, and a string URL will be pushed into history as-is</li>
|
||||||
|
/// <li><c>replace:</c> and <c>true</c>, <c>false</c>, or a URL; same as <c>push:</c>, but replaces in history
|
||||||
|
/// instead</li>
|
||||||
|
/// <li><c>transition:</c> and <c>true</c> or <c>false</c>; whether to use view transitions</li>
|
||||||
|
/// </ul>
|
||||||
|
/// If only the type of swap is being changed, pass an empty string to this parameter.
|
||||||
|
/// </param>
|
||||||
|
/// <seealso href="https://four.htmx.org/reference/attributes/hx-status">Documentation</seealso>
|
||||||
|
let _hxStatus code swap action =
|
||||||
|
let range = $"%s{code}xx".Substring(0, 3)
|
||||||
|
attr $"hx-status:{range}" ($"swap:%s{swap} %s{action}".Trim())
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="swap">The type of swap to perform (use <c>HxSwap</c> values)</param>
|
||||||
|
/// <returns>A configured <c>hx-swap</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
|
||||||
|
let _hxSwap swap =
|
||||||
|
attr "hx-swap" swap
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Controls how the response content is swapped into the DOM (e.g. <c>outerHTML</c> or <c>beforeEnd</c>), enabling
|
||||||
|
/// CSS transitions
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="swap">The type of swap to perform (use <c>HxSwap</c> values)</param>
|
||||||
|
/// <returns>A configured <c>hx-swap</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
|
||||||
|
let _hxSwapWithTransition swap =
|
||||||
|
_hxSwap $"%s{swap} transition:true"
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="swap">
|
||||||
|
/// <ul>
|
||||||
|
/// <li>"true" to mark this as an OOB swap</li>
|
||||||
|
/// <li>Any <c>HxSwap</c> value</li>
|
||||||
|
/// <li>Any <c>HxSwap</c> value, followed by a colon (<c>:</c>) and a CSS selector</li>
|
||||||
|
/// </ul>
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A configured <c>hx-swap-oob</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-swap-oob/">Documentation</seealso>
|
||||||
|
let _hxSwapOob swap =
|
||||||
|
attr "hx-swap-oob" swap
|
||||||
|
|
||||||
|
/// <summary>Synchronize events based on another element</summary>
|
||||||
|
/// <param name="selector">A CSS selector for the element with which this one should sync</param>
|
||||||
|
/// <param name="action">The request synchronization action to perform (use <c>HxSync</c> values)</param>
|
||||||
|
/// <returns>A configured <c>hx-sync</c> attribute</returns>
|
||||||
|
/// <seealso cref="HxSync" />
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
|
||||||
|
let _hxSync selector action =
|
||||||
|
attr "hx-sync" $"%s{selector}:%s{action}"
|
||||||
|
|
||||||
|
/// <summary>Specifies the target element to be swapped</summary>
|
||||||
|
/// <param name="selector">A CSS selector or relative reference (or both) to identify the target</param>
|
||||||
|
/// <returns>A configured <c>hx-target</c> attribute</returns>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-target/">Documentation</seealso>
|
||||||
|
let _hxTarget selector =
|
||||||
|
attr "hx-target" selector
|
||||||
|
|
||||||
|
/// <summary>Specifies the event that triggers the request</summary>
|
||||||
|
/// <param name="spec">The trigger specification (use <c>HxTrigger</c> to create)</param>
|
||||||
|
/// <returns>A configured <c>hx-trigger</c> attribute</returns>
|
||||||
|
/// <seealso cref="HxTrigger" />
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
|
||||||
|
let _hxTrigger spec =
|
||||||
|
attr "hx-trigger" spec
|
||||||
|
|
||||||
|
/// <summary>Validate an input element (uses HTML5 validation API)</summary>
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-validate/">Documentation</seealso>
|
||||||
|
let _hxValidate =
|
||||||
|
flag "hx-validate"
|
||||||
|
|
||||||
|
/// <summary>Adds to the parameters that will be submitted with the request</summary>
|
||||||
|
/// <param name="values">The values for the parameters (use <c>HxVals.From</c> to create)</param>
|
||||||
|
/// <returns>A configured <c>hx-vals</c> attribute</returns>
|
||||||
|
/// <seealso cref="HxVals.From" />
|
||||||
|
/// <seealso href="https://htmx.org/attributes/hx-vals/">Documentation</seealso>
|
||||||
|
let _hxVals values =
|
||||||
|
attr "hx-vals" values
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Modifiers for htmx attributes</summary>
|
||||||
|
[<AutoOpen>]
|
||||||
|
module HxModifiers =
|
||||||
|
|
||||||
|
/// <summary>Append this value to any inherited value of the attribute</summary>
|
||||||
|
/// <param name="attrib">The attribute to which <c>:append</c> should be applied</param>
|
||||||
|
/// <returns>The given attribute with <c>:append</c> applied</returns>
|
||||||
|
let hxAppend attrib =
|
||||||
|
match attrib with
|
||||||
|
| KeyValue (name, value) -> attr $"{name}:append" value
|
||||||
|
| Boolean name -> flag $"{name}:append"
|
||||||
|
|
||||||
|
/// <summary>Explicitly propagate inheritance for the value for this attribute</summary>
|
||||||
|
/// <param name="attrib">The attribute whose value should be inherited</param>
|
||||||
|
/// <returns>The given attribute with <c>:inherited</c> applied</returns>
|
||||||
|
let hxInherited attrib =
|
||||||
|
match attrib with
|
||||||
|
| KeyValue (name, value) -> attr $"{name}:inherited" value
|
||||||
|
| Boolean name -> flag $"{name}:inherited"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>htmx-specific HTML tags</summary>
|
||||||
|
[<AutoOpen>]
|
||||||
|
module HxTags =
|
||||||
|
|
||||||
|
/// <summary>An <c>hx-partial</c> tag</summary>
|
||||||
|
let hxPartial = tag "hx-partial"
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Script tags to pull htmx into a web page</summary>
|
||||||
|
module Script =
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the package-provided version of htmx</summary>
|
||||||
|
let local = script [ _src StaticAssetUrl.htmx ] []
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
|
||||||
|
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
|
||||||
|
let cdnMinified =
|
||||||
|
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js"
|
||||||
|
_integrity "sha384-BvJpBiO8Kh31EqtJe5DRIeWrHWnCGkwytKs9NKFi86Hhw96dEqdEMzZDeK9iEGTc"
|
||||||
|
_crossorigin "anonymous" ] []
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the unminified version from jsdelivr.net</summary>
|
||||||
|
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
|
||||||
|
let cdnUnminified =
|
||||||
|
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js"
|
||||||
|
_integrity "sha384-ESzWv77gBOAGtF3d7B8QiQ786cghRRsyhuYVVJGsrFGxwT0Dj1fLxReX9Ul6t4n9"
|
||||||
|
_crossorigin "anonymous" ] []
|
||||||
|
|
||||||
|
/// <summary>Script tags to load the htmax bundle</summary>
|
||||||
|
module Max =
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the package-provided version of the htmx-plus-extensions bundle</summary>
|
||||||
|
let local = script [ _src StaticAssetUrl.htmax ] []
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the minified htmx-plus-extensions bundle from jsdelivr.net</summary>
|
||||||
|
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
|
||||||
|
let cdnMinified =
|
||||||
|
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.min.js"
|
||||||
|
_integrity "sha384-F2+P/m0g8+he+KLpgsCVZlZGvOx2z83bU4rmmISSNt7br4A0RTcJ7ddMrz0v45Nv"
|
||||||
|
_crossorigin "anonymous" ] []
|
||||||
|
|
||||||
|
/// <summary>Script tag to load the unminified htmx-plus-extensions bundle from jsdelivr.net</summary>
|
||||||
|
/// <remarks>Ensure <c>cdn.jsdelivr.net</c> is in your CSP <c>script-src</c> list (if applicable)</remarks>
|
||||||
|
let cdnUnminified =
|
||||||
|
script [ _src $"https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmax.js"
|
||||||
|
_integrity "sha384-wrTFbAj755gAdIPPR9n4aAVMkLWkzxjr1Zj+8D4kgr2App4h9doO6SXBH9wpivij"
|
||||||
|
_crossorigin "anonymous" ] []
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>Functions to extract and render an HTML fragment from a document</summary>
|
||||||
|
[<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>– ID {nodeId} not found –</em>"
|
||||||
|
|
||||||
|
/// <summary>Find the node with the named ID</summary>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute to find</param>
|
||||||
|
/// <param name="node">The node tree to search</param>
|
||||||
|
/// <returns>The node with the requested <c>id</c> attribute, or <c>None</c> if it was not found</returns>
|
||||||
|
let rec findIdNode nodeId (node: XmlNode) : XmlNode option =
|
||||||
|
match node with
|
||||||
|
| 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 (findIdNode nodeId)
|
||||||
|
|
||||||
|
/// <summary>Functions to render a fragment as a string</summary>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module AsString =
|
||||||
|
|
||||||
|
/// <summary>Render to HTML for the given ID</summary>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="nodes">The node trees to search</param>
|
||||||
|
/// <returns>The HTML for the given <c>id</c> node, or an error message if it was not found</returns>
|
||||||
|
let htmlFromNodes nodeId (nodes: XmlNode list) =
|
||||||
|
match nodes |> List.tryPick (findIdNode nodeId) with
|
||||||
|
| Some idNode -> RenderView.AsString.htmlNode idNode
|
||||||
|
| None -> nodeNotFound nodeId
|
||||||
|
|
||||||
|
/// <summary>Render to HTML for the given ID</summary>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="node">The node tree to search</param>
|
||||||
|
/// <returns>The HTML for the given <c>id</c> node, or an error message if it was not found</returns>
|
||||||
|
let htmlFromNode nodeId node =
|
||||||
|
match findIdNode nodeId node with
|
||||||
|
| Some idNode -> RenderView.AsString.htmlNode idNode
|
||||||
|
| None -> nodeNotFound nodeId
|
||||||
|
|
||||||
|
/// <summary>Functions to render a fragment as bytes</summary>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module AsBytes =
|
||||||
|
|
||||||
|
let private utf8 = System.Text.Encoding.UTF8
|
||||||
|
|
||||||
|
/// <summary>Render to bytes for the given ID</summary>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="nodes">The node trees to search</param>
|
||||||
|
/// <returns>The bytes for the given <c>id</c> node, or an error message if it was not found</returns>
|
||||||
|
let htmlFromNodes nodeId (nodes: XmlNode list) =
|
||||||
|
match nodes |> List.tryPick (findIdNode nodeId) with
|
||||||
|
| Some idNode -> RenderView.AsBytes.htmlNode idNode
|
||||||
|
| None -> nodeNotFound nodeId |> utf8.GetBytes
|
||||||
|
|
||||||
|
/// <summary>Render to bytes for the given ID</summary>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="node">The node tree to search</param>
|
||||||
|
/// <returns>The bytes for the given <c>id</c> node, or an error message if it was not found</returns>
|
||||||
|
let htmlFromNode nodeId node =
|
||||||
|
match findIdNode nodeId node with
|
||||||
|
| Some idNode -> RenderView.AsBytes.htmlNode idNode
|
||||||
|
| None -> nodeNotFound nodeId |> utf8.GetBytes
|
||||||
|
|
||||||
|
/// <summary>Functions to render a fragment into a StringBuilder</summary>
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module IntoStringBuilder =
|
||||||
|
|
||||||
|
/// <summary>Render HTML into a <c>StringBuilder</c> for the given ID</summary>
|
||||||
|
/// <param name="sb">The <c>StringBuilder</c> into which the bytes will be rendered</param>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="nodes">The node trees to search</param>
|
||||||
|
let htmlFromNodes sb nodeId (nodes: XmlNode list) =
|
||||||
|
match nodes |> List.tryPick (findIdNode nodeId) with
|
||||||
|
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
|
||||||
|
| None -> nodeNotFound nodeId |> sb.Append |> ignore
|
||||||
|
|
||||||
|
/// <summary>Render HTML into a <c>StringBuilder</c> for the given ID</summary>
|
||||||
|
/// <param name="sb">The <c>StringBuilder</c> into which the bytes will be rendered</param>
|
||||||
|
/// <param name="nodeId">The <c>id</c> attribute for the node to be rendered</param>
|
||||||
|
/// <param name="node">The node tree to search</param>
|
||||||
|
let htmlFromNode sb nodeId node =
|
||||||
|
match findIdNode nodeId node with
|
||||||
|
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
|
||||||
|
| None -> nodeNotFound nodeId |> sb.Append |> ignore
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
## Giraffe.ViewEngine.Htmx
|
||||||
|
|
||||||
|
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
|
||||||
|
|
||||||
|
**htmx version: 4.0.0**
|
||||||
|
|
||||||
|
_Upgrading from v2.x: see [the migration guide](https://four.htmx.org/docs#migrating-from-htmx-2x-to-4x) for changes, which are plentiful. htmx switches from `XMLHTTPRequest` to `fetch`, and many changes are related to the new event cycle._
|
||||||
|
|
||||||
|
_Inheritance is now explicit; to have an attribute's value inherited to its children, wrap the attribute in `hxInherited` (ex. `hxInherited (_hxTarget "#main")`). Values can be appended to inherited values as well using the `hxAppend` modifier._
|
||||||
|
|
||||||
|
_Several constructs have been removed in this release. If you encounter compile errors, install version `4.0.0-beta5`; it will instead flag these with warnings and suggestions on how to mitigate them. Once the warnings are gone, you can reinstall this version._
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install the package.
|
||||||
|
1. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`.
|
||||||
|
1. If the client is using the `htmax` bundle, also `open Giraffe.ViewEngine.Htmax`.
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
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
|
||||||
|
let autoload =
|
||||||
|
div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Support modules include:
|
||||||
|
- `HxConfig` _(new in v4, replaces `HxRequest`)_
|
||||||
|
- `HxEncoding`
|
||||||
|
- `HxHeaders`
|
||||||
|
- `HxSwap` (requires `open Giraffe.Htmx`)
|
||||||
|
- `HxTrigger`
|
||||||
|
- `HxVals`
|
||||||
|
|
||||||
|
`Htmx.Script.local` creates an `XmlNode` to load the package-provided htmx library. There are also two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.cdnMinified` loads the minified version, and `Htmx.Script.cdnUnminified` loads the unminified version (useful for debugging). htmx v4 also distributes a "max" bundle which contains some common extensions; these are available with the same names under `Htmx.Script.Max`.
|
||||||
|
|
||||||
|
_NOTE: When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`._
|
||||||
|
|
||||||
|
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).
|
||||||
|
|
||||||
|
### Learn
|
||||||
|
|
||||||
|
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:
|
||||||
|
- `HxConfig` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
HxConfig.Configure [ HxRequest.Timeout 500 ] |> _hxConfig
|
||||||
|
```
|
||||||
|
- `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
HxTrigger.Click
|
||||||
|
|> HxTrigger.Filter.Shift
|
||||||
|
|> HxTrigger.FromDocument
|
||||||
|
|> HxTrigger.Delay "3s"
|
||||||
|
|> _hxTrigger
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
(HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click
|
||||||
|
|> _hxTrigger
|
||||||
|
```
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Executable
+7
@@ -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 .
|
||||||
Reference in New Issue
Block a user