Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfd8d7eb68 | ||
|
|
8f93aa4d35 |
@@ -6,19 +6,21 @@
|
||||
|
||||
htmx uses attributes and HTTP headers to attain its interactivity; the libraries here contain extensions to both Giraffe and Giraffe View Engine to enable strongly-typed development of htmx applications.
|
||||
|
||||
> **NOTE** htmx v4 is curently in beta; this library is supporting it on the [`htmx-v4` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v4/). Beta versions of the v4 library include warnings about obsolete constructs. The htmx project intends to continue supporting the 2.x line (just as they still do the 1.x line, which we track on the [`htmx-v1` branch](https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx/src/branch/htmx-v1/)); the upgrade is available, not forced. See the [htmx v4 migration docs](https://four.htmx.org/docs#migration) for more information.
|
||||
|
||||
## Installation
|
||||
|
||||
`Giraffe.Htmx` provides extensions that facilitate using htmx on the server side, primarily reading and setting headers. `Giraffe.ViewEngine.Htmx` provides attributes and helpers to produce views that utilize htmx. Both can be installed from NuGet via standard methods.
|
||||
|
||||
| Server Side | View Engine |
|
||||
|------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [](https://www.nuget.org/packages/Giraffe.Htmx/) | [](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/) |
|
||||
| Server Side | View Engine |
|
||||
|---|---|
|
||||
|[](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)_.
|
||||
Both of these packages will also install `Giraffe.Htmx.Common`, which has some common definitions and provides a local-to-your-project version of the htmx JavaScript _(as of v2.0.8)_.
|
||||
|
||||
## Server Side (`Giraffe.Htmx`)
|
||||
|
||||
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://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.
|
||||
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.
|
||||
|
||||
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...
|
||||
|
||||
@@ -33,7 +35,7 @@ let result view : HttpHandler =
|
||||
full view
|
||||
```
|
||||
|
||||
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.
|
||||
htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response.
|
||||
|
||||
```fsharp
|
||||
let theHandler : HttpHandler =
|
||||
@@ -44,11 +46,11 @@ let theHandler : HttpHandler =
|
||||
|
||||
Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well.
|
||||
|
||||
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library, 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.)
|
||||
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library. This can be used in code, Razor templates, etc. (If you're using Giraffe.ViewEngine, see below.)
|
||||
|
||||
## View Engine (`Giraffe.ViewEngine.Htmx`)
|
||||
|
||||
As htmx uses [attributes](https://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 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 an example, creating a `div` that loads data once the HTML is rendered:
|
||||
|
||||
@@ -70,11 +72,11 @@ let shiftClick =
|
||||
]
|
||||
```
|
||||
|
||||
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.
|
||||
If you want to use the package-provided htmx library, `Htmx.Script.local` will create the `script` tag for you. To load htmx from jsDelivr, `Htmx.Script.cdnMinified` or `Htmx.Script.cdnUnminified` can be used to load the script in your HTML trees. In this case, if you are using a Content Security Policy (CSP) header, `cdn.jsdelivr.net` will need to be added to the `script-src` list.
|
||||
|
||||
## Feedback / Help
|
||||
|
||||
The author hangs out in the #dotnet-htmx channel (and most others) of the [htmx Discord server](https://htmx.org/discord) and the #web channel (and most others) of the [F# Community Discord server](https://discord.gg/R6n7c54).
|
||||
The author hangs out in the #dotnet-htmx channel (and most others) of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/).
|
||||
|
||||
## Thanks
|
||||
|[<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)|
|
||||
|
||||
+10
-64
@@ -3,15 +3,10 @@
|
||||
module Giraffe.Htmx.Common
|
||||
|
||||
/// <summary>The version of htmx embedded in the package</summary>
|
||||
let HtmxVersion = "4.0.0"
|
||||
let HtmxVersion = "2.0.10"
|
||||
|
||||
/// <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>The path for the provided htmx script</summary>
|
||||
let internal htmxLocalScript = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
|
||||
|
||||
/// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary>
|
||||
/// <param name="pairs">The key/value pairs to be serialized to JSON</param>
|
||||
@@ -30,8 +25,8 @@ let internal toLowerBool (boolValue: bool) =
|
||||
|
||||
|
||||
/// <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>
|
||||
/// <remarks>May be combined with <c>swap</c> / <c>settle</c> / <c>scroll</c> / <c>show</c> config)</remarks>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-swap/">Documentation</seealso>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxSwap =
|
||||
|
||||
@@ -43,71 +38,22 @@ module HxSwap =
|
||||
[<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
|
||||
let BeforeBegin = "beforebegin"
|
||||
|
||||
/// <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
|
||||
let AfterBegin = "afterbegin"
|
||||
|
||||
/// <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
|
||||
let BeforeEnd = "beforeend"
|
||||
|
||||
/// <summary>Insert the response after the target element</summary>
|
||||
[<Literal>]
|
||||
let After = "after"
|
||||
let AfterEnd = "afterend"
|
||||
|
||||
/// <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>
|
||||
/// <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"
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
## Giraffe.Htmx.Common
|
||||
|
||||
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx and htmax as static web assets, allowing them to be loaded from your local (or published) project.
|
||||
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. It also contains htmx as a static web asset, allowing it 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._
|
||||
**htmx version: 2.0.10**
|
||||
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -2,11 +2,11 @@
|
||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
|
||||
<VersionPrefix>4.0.0</VersionPrefix>
|
||||
<VersionPrefix>2.0.10</VersionPrefix>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<PackageReleaseNotes>htmx 4.0.0 Initial Release
|
||||
<PackageReleaseNotes>- Updates provided script and CDN links to serve htmx 2.0.10 (no header or attribute changes)
|
||||
|
||||
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.
|
||||
NOTE: As of 2.0.6, the CDN for htmx changed from unpkg.com to cdn.jsdelivr.net; sites with Content-Security-Policy headers will want to update their allowed script-src domains accordingly
|
||||
</PackageReleaseNotes>
|
||||
<Authors>danieljsummers</Authors>
|
||||
<Company>Bit Badger Solutions</Company>
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<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>
|
||||
@@ -14,8 +14,8 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Giraffe" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
<PackageReference Include="Giraffe" Version="6.4.0" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
+38
-68
@@ -4,14 +4,6 @@ open Microsoft.AspNetCore.Http
|
||||
open Microsoft.Extensions.Primitives
|
||||
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
|
||||
let private hdr (headers : IHeaderDictionary) hdr =
|
||||
match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0]
|
||||
@@ -31,13 +23,7 @@ type IHeaderDictionary with
|
||||
member this.HxHistoryRestoreRequest
|
||||
with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
|
||||
|
||||
/// <summary><c>true</c> if the request has been fired by the preload extension</summary>
|
||||
/// <remarks><c>preload</c> is part of the htmax htmx-plus-extensions bundle</remarks>
|
||||
member this.HxPreloaded
|
||||
with get () = hdr this "HX-Preloaded" |> Option.map bool.Parse
|
||||
|
||||
/// <summary>The user response to an <c>hx-prompt</c></summary>
|
||||
/// <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"
|
||||
|
||||
@@ -45,51 +31,17 @@ type IHeaderDictionary with
|
||||
member this.HxRequest
|
||||
with get () = hdr this "HX-Request" |> Option.map bool.Parse
|
||||
|
||||
/// <summary>The ID of the request (WebSocket extension requests only)</summary>
|
||||
/// <remarks><c>hx-ws</c> is part of the htmax htmx-plus-extensions bundle</remarks>
|
||||
member this.HxRequestId
|
||||
with get () = hdr this "HX-Request-ID"
|
||||
|
||||
/// <summary>The request type sent by htmx</summary>
|
||||
/// <seealso cref="HxRequestTypes" />
|
||||
member this.HxRequestType
|
||||
with get () =
|
||||
match hdr this "HX-Request-Type" with
|
||||
| Some typ when typ = "full" -> Some HxFullRequest
|
||||
| Some typ when typ = "partial" -> Some HxPartialRequest
|
||||
| Some _ -> None
|
||||
| None -> None
|
||||
|
||||
/// <summary>The tag name (fst) and <c>id</c> attribute (snd) of the element triggering this request</summary>
|
||||
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
|
||||
with get () = hdr this "HX-Target"
|
||||
|
||||
/// <summary>The <c>id</c> attribute of the triggered element if it exists</summary>
|
||||
member this.HxTrigger
|
||||
with get () = hdr this "HX-Trigger"
|
||||
|
||||
/// <summary>The <c>name</c> attribute of the triggered element if it exists</summary>
|
||||
member this.HxTriggerName
|
||||
with get () = hdr this "HX-Trigger-Name"
|
||||
|
||||
|
||||
/// Extensions for the request object
|
||||
@@ -110,14 +62,6 @@ module Handlers =
|
||||
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
/// <summary>Instruct htmx to download a response from another path / URL</summary>
|
||||
/// <param name="path">The path or URL where the downloadable content is found</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Download</c> header set</returns>
|
||||
/// <remarks>This requires the client-side <c>hx-download</c> extension (included in the htmax bundle)</remarks>
|
||||
/// <seealso href="https://four.htmx.org/extensions/hx-download#hx-download-header">Documentation</seealso>
|
||||
let withHxDownload (path: string) : HttpHandler =
|
||||
setHttpHeader "HX-Download" path
|
||||
|
||||
/// <summary>Instruct htmx to perform a client-side redirect for content</summary>
|
||||
/// <param name="path">The path where the content should be found</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Location</c> header set</returns>
|
||||
@@ -199,6 +143,34 @@ module Handlers =
|
||||
let withHxTriggerMany evts : HttpHandler =
|
||||
toJson evts |> setHttpHeader "HX-Trigger"
|
||||
|
||||
/// <summary>Allows you to trigger a single client side event after changes have settled</summary>
|
||||
/// <param name="evt">The call to the event that should be triggered</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
let withHxTriggerAfterSettle (evt: string) : HttpHandler =
|
||||
setHttpHeader "HX-Trigger-After-Settle" evt
|
||||
|
||||
/// <summary>Allows you to trigger multiple client side events after changes have settled</summary>
|
||||
/// <param name="evts">The calls to events that should be triggered</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Settle</c> header set for all given events</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
let withHxTriggerManyAfterSettle evts : HttpHandler =
|
||||
toJson evts |> setHttpHeader "HX-Trigger-After-Settle"
|
||||
|
||||
/// <summary>Allows you to trigger a single client side event after DOM swapping occurs</summary>
|
||||
/// <param name="evt">The call to the event that should be triggered</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
let withHxTriggerAfterSwap (evt: string) : HttpHandler =
|
||||
setHttpHeader "HX-Trigger-After-Swap" evt
|
||||
|
||||
/// <summary>Allows you to trigger multiple client side events after DOM swapping occurs</summary>
|
||||
/// <param name="evts">The calls to events that should be triggered</param>
|
||||
/// <returns>An HTTP handler with the <c>HX-Trigger-After-Swap</c> header set for all given events</returns>
|
||||
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
|
||||
let withHxTriggerManyAfterSwap evts : HttpHandler =
|
||||
toJson evts |> setHttpHeader "HX-Trigger-After-Swap"
|
||||
|
||||
|
||||
/// <summary>Load the package-provided version of the htmx script</summary>
|
||||
[<RequireQualifiedAccess>]
|
||||
@@ -208,7 +180,5 @@ module HtmxScript =
|
||||
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>"""
|
||||
let local = HtmlString $"""<script src="{htmxLocalScript}"></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>"""
|
||||
|
||||
+6
-8
@@ -2,11 +2,9 @@
|
||||
|
||||
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**
|
||||
**htmx version: 2.0.10**
|
||||
|
||||
_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._
|
||||
_Upgrading from v1.x: the [migration guide](https://htmx.org/migration-guide-htmx-1/) does not currently specify any request or response header changes. This means that there are no required code changes in moving from v1.* to v2.*._
|
||||
|
||||
### Setup
|
||||
|
||||
@@ -20,9 +18,9 @@ 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
|
||||
match ctx.HxPrompt with
|
||||
| Some prompt -> ... // do something with the text the user provided
|
||||
| None -> ... // no text provided
|
||||
```
|
||||
|
||||
To set a response header:
|
||||
@@ -36,7 +34,7 @@ let myHandler : HttpHander =
|
||||
|
||||
The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information.
|
||||
|
||||
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`; to load the htmax bundle, use `HtmxScript.localMax`.
|
||||
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`.
|
||||
|
||||
### Learn
|
||||
|
||||
|
||||
+7
-56
@@ -6,25 +6,9 @@ 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"
|
||||
Expect.equal HtmxVersion "2.0.10" "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" [
|
||||
@@ -34,55 +18,22 @@ let swap =
|
||||
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"
|
||||
Expect.equal HxSwap.BeforeBegin "beforebegin" "Before Begin swap value incorrect"
|
||||
}
|
||||
test "BeforeEnd is correct" {
|
||||
Expect.equal HxSwap.BeforeEnd HxSwap.Append "Before End swap value incorrect"
|
||||
Expect.equal HxSwap.BeforeEnd "beforeend" "Before End swap value incorrect"
|
||||
}
|
||||
test "After is correct" {
|
||||
Expect.equal HxSwap.After "after" "After swap value incorrect"
|
||||
test "AfterBegin is correct" {
|
||||
Expect.equal HxSwap.AfterBegin "afterbegin" "After Begin swap value incorrect"
|
||||
}
|
||||
test "AfterEnd is correct" {
|
||||
Expect.equal HxSwap.AfterEnd HxSwap.After "After End swap value incorrect"
|
||||
}
|
||||
test "Delete is correct" {
|
||||
Expect.equal HxSwap.Delete "delete" "Delete swap value incorrect"
|
||||
Expect.equal HxSwap.AfterEnd "afterend" "After End 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 ]
|
||||
let allTests = testList "Htmx.Common" [ version; swap ]
|
||||
|
||||
+67
-142
@@ -3,6 +3,7 @@ module Htmx
|
||||
open System
|
||||
open Expecto
|
||||
open Giraffe.Htmx
|
||||
open Microsoft.AspNetCore.Html
|
||||
open Microsoft.AspNetCore.Http
|
||||
open NSubstitute
|
||||
|
||||
@@ -73,30 +74,6 @@ let dictExtensions =
|
||||
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>()
|
||||
@@ -135,125 +112,49 @@ let dictExtensions =
|
||||
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" {
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Target", "div#leItem")
|
||||
dic.Add("HX-Target", "#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"
|
||||
Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect"
|
||||
}
|
||||
test "succeeds when the header is present and ID is blank" {
|
||||
]
|
||||
testList "HxTrigger" [
|
||||
test "succeeds when the header is not present" {
|
||||
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"
|
||||
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
|
||||
}
|
||||
test "succeeds when the header is present and ID is missing" {
|
||||
test "succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Target", "aside")
|
||||
dic.Add("HX-Trigger", "#trig")
|
||||
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"
|
||||
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect"
|
||||
}
|
||||
]
|
||||
testList "HxTriggerName" [
|
||||
test "succeeds when the header is not present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
|
||||
Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned"
|
||||
}
|
||||
test "HxTriggerName succeeds when the header is present" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
dic.Add("HX-Trigger-Name", "click")
|
||||
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present"
|
||||
Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -307,14 +208,6 @@ 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()
|
||||
@@ -422,6 +315,44 @@ let handlers =
|
||||
Expect.equal
|
||||
dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerAfterSettle succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
|
||||
Expect.isTrue
|
||||
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present"
|
||||
Expect.equal dic["HX-Trigger-After-Settle"].[0] "byTheWay" "The HX-Trigger-After-Settle value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerManyAfterSettle succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
|
||||
Expect.isTrue
|
||||
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present"
|
||||
Expect.equal
|
||||
dic["HX-Trigger-After-Settle"].[0] """{ "oof": "ouch", "hmm": "uh" }"""
|
||||
"The HX-Trigger-After-Settle value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerAfterSwap succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerAfterSwap "justASec" next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present"
|
||||
Expect.equal dic["HX-Trigger-After-Swap"].[0] "justASec" "The HX-Trigger-After-Swap value was incorrect"
|
||||
}
|
||||
testTask "withHxTriggerManyAfterSwap succeeds" {
|
||||
let ctx = Substitute.For<HttpContext>()
|
||||
let dic = HeaderDictionary()
|
||||
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx
|
||||
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present"
|
||||
Expect.equal
|
||||
dic["HX-Trigger-After-Swap"].[0] """{ "this": "1", "that": "2" }"""
|
||||
"The HX-Trigger-After-Swap value was incorrect"
|
||||
}
|
||||
]
|
||||
|
||||
/// Tests for the HtmxScript module
|
||||
@@ -433,12 +364,6 @@ let script =
|
||||
$"""<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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
open Expecto
|
||||
|
||||
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests; ViewEngineMax.allTests ]
|
||||
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests ]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args = runTestsWithCLIArgs [] args allTests
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<Compile Include="Common.fs" />
|
||||
<Compile Include="Htmx.fs" />
|
||||
<Compile Include="ViewEngine.fs" />
|
||||
<Compile Include="ViewEngineMax.fs" />
|
||||
<Compile Include="Program.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -19,9 +18,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Expecto" />
|
||||
<PackageReference Include="NSubstitute" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
<PackageReference Include="Expecto" Version="10.2.1" />
|
||||
<PackageReference Include="NSubstitute" Version="5.1.0" />
|
||||
<PackageReference Update="FSharp.Core" Version="8.0.300" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+398
-244
@@ -1,51 +1,9 @@
|
||||
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" [
|
||||
@@ -57,7 +15,6 @@ let hxEncoding =
|
||||
}
|
||||
]
|
||||
|
||||
/// Tests for the HxEvent module
|
||||
let hxEvent =
|
||||
testList "HxEvent" [
|
||||
testList "Abort" [
|
||||
@@ -68,58 +25,23 @@ let hxEvent =
|
||||
Expect.equal (Abort.ToHxOnString()) "abort" "Abort hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterCleanup" [
|
||||
testList "AfterOnLoad" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string AfterCleanup) "afterCleanup" "AfterCleanup event name not correct"
|
||||
Expect.equal (string AfterOnLoad) "afterOnLoad" "AfterOnLoad event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (AfterCleanup.ToHxOnString()) "after:cleanup" "AfterCleanup hx-on event name not correct"
|
||||
Expect.equal (AfterOnLoad.ToHxOnString()) "after-on-load" "AfterOnLoad hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterHistoryUpdate" [
|
||||
testList "AfterProcessNode" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string AfterHistoryUpdate) "afterHistoryUpdate" "AfterHistoryUpdate event name not correct"
|
||||
Expect.equal (string AfterProcessNode) "afterProcessNode" "AfterProcessNode 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"
|
||||
(AfterProcessNode.ToHxOnString())
|
||||
"after-process-node"
|
||||
"AfterProcessNode hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterRequest" [
|
||||
@@ -127,7 +49,7 @@ let hxEvent =
|
||||
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"
|
||||
Expect.equal (AfterRequest.ToHxOnString()) "after-request" "AfterRequest hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterSettle" [
|
||||
@@ -135,7 +57,7 @@ let hxEvent =
|
||||
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"
|
||||
Expect.equal (AfterSettle.ToHxOnString()) "after-settle" "AfterSettle hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterSwap" [
|
||||
@@ -143,57 +65,38 @@ let hxEvent =
|
||||
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"
|
||||
Expect.equal (AfterSwap.ToHxOnString()) "after-swap" "AfterSwap hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeCleanup" [
|
||||
testList "BeforeCleanupElement" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeCleanup) "beforeCleanup" "BeforeCleanup event name not correct"
|
||||
Expect.equal
|
||||
(string BeforeCleanupElement) "beforeCleanupElement" "BeforeCleanupElement event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeCleanup.ToHxOnString()) "before:cleanup" "BeforeCleanup hx-on event name not correct"
|
||||
(BeforeCleanupElement.ToHxOnString())
|
||||
"before-cleanup-element"
|
||||
"BeforeCleanupElement hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeHistoryUpdate" [
|
||||
testList "BeforeOnLoad" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string BeforeHistoryUpdate) "beforeHistoryUpdate" "BeforeHistoryUpdate event name not correct"
|
||||
Expect.equal (string BeforeOnLoad) "beforeOnLoad" "BeforeOnLoad event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (BeforeOnLoad.ToHxOnString()) "before-on-load" "BeforeOnLoad hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeProcessNode" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeProcessNode) "beforeProcessNode" "BeforeProcessNode event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(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"
|
||||
(BeforeProcessNode.ToHxOnString())
|
||||
"before-process-node"
|
||||
"BeforeProcessNode hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeRequest" [
|
||||
@@ -202,16 +105,7 @@ let hxEvent =
|
||||
}
|
||||
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"
|
||||
(BeforeRequest.ToHxOnString()) "before-request" "BeforeRequest hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeSwap" [
|
||||
@@ -219,7 +113,15 @@ let hxEvent =
|
||||
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"
|
||||
Expect.equal (BeforeSwap.ToHxOnString()) "before-swap" "BeforeSwap hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeSend" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeSend) "beforeSend" "BeforeSend event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (BeforeSend.ToHxOnString()) "before-send" "BeforeSend hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "ConfigRequest" [
|
||||
@@ -228,7 +130,7 @@ let hxEvent =
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(ConfigRequest.ToHxOnString()) "config:request" "ConfigRequest hx-on event name not correct"
|
||||
(ConfigRequest.ToHxOnString()) "config-request" "ConfigRequest hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "Confirm" [
|
||||
@@ -239,21 +141,146 @@ let hxEvent =
|
||||
Expect.equal (Confirm.ToHxOnString()) "confirm" "Confirm hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "Error" [
|
||||
testList "HistoryCacheError" [
|
||||
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"
|
||||
Expect.equal (string HistoryCacheError) "historyCacheError" "HistoryCacheError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(FinallyRequest.ToHxOnString()) "finally:request" "FinallyRequest hx-on event name not correct"
|
||||
(HistoryCacheError.ToHxOnString())
|
||||
"history-cache-error"
|
||||
"HistoryCacheError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "HistoryCacheMiss" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string HistoryCacheMiss) "historyCacheMiss" "HistoryCacheMiss event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(HistoryCacheMiss.ToHxOnString())
|
||||
"history-cache-miss"
|
||||
"HistoryCacheMiss hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "HistoryCacheMissError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string HistoryCacheMissError)
|
||||
"historyCacheMissError"
|
||||
"HistoryCacheMissError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(HistoryCacheMissError.ToHxOnString())
|
||||
"history-cache-miss-error"
|
||||
"HistoryCacheMissError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "HistoryCacheMissLoad" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string HistoryCacheMissLoad) "historyCacheMissLoad" "HistoryCacheMissLoad event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(HistoryCacheMissLoad.ToHxOnString())
|
||||
"history-cache-miss-load"
|
||||
"HistoryCacheMissLoad hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "HistoryRestore" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string HistoryRestore) "historyRestore" "HistoryRestore event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(HistoryRestore.ToHxOnString()) "history-restore" "HistoryRestore hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeHistorySave" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeHistorySave) "beforeHistorySave" "BeforeHistorySave event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeHistorySave.ToHxOnString())
|
||||
"before-history-save"
|
||||
"BeforeHistorySave hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "Load" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string Load) "load" "Load event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (Load.ToHxOnString()) "load" "Load hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "NoSseSourceError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string NoSseSourceError) "noSSESourceError" "NoSseSourceError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(NoSseSourceError.ToHxOnString())
|
||||
"no-sse-source-error"
|
||||
"NoSseSourceError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "OnLoadError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string OnLoadError) "onLoadError" "OnLoadError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (OnLoadError.ToHxOnString()) "on-load-error" "OnLoadError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "OobAfterSwap" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string OobAfterSwap) "oobAfterSwap" "OobAfterSwap event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (OobAfterSwap.ToHxOnString()) "oob-after-swap" "OobAfterSwap hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "OobBeforeSwap" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string OobBeforeSwap) "oobBeforeSwap" "OobBeforeSwap event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(OobBeforeSwap.ToHxOnString()) "oob-before-swap" "OobBeforeSwap hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "OobErrorNoTarget" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string OobErrorNoTarget) "oobErrorNoTarget" "OobErrorNoTarget event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(OobErrorNoTarget.ToHxOnString())
|
||||
"oob-error-no-target"
|
||||
"OobErrorNoTarget hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "Prompt" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string Prompt) "prompt" "Prompt event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (Prompt.ToHxOnString()) "prompt" "Prompt hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "PushedIntoHistory" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string PushedIntoHistory) "pushedIntoHistory" "PushedIntoHistory event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(PushedIntoHistory.ToHxOnString())
|
||||
"pushed-into-history"
|
||||
"PushedIntoHistory hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "ResponseError" [
|
||||
@@ -262,7 +289,121 @@ let hxEvent =
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(ResponseError.ToHxOnString()) "response:error" "ResponseError hx-on event name not correct"
|
||||
(ResponseError.ToHxOnString()) "response-error" "ResponseError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SendError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SendError) "sendError" "SendError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SendError.ToHxOnString()) "send-error" "SendError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SseError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SseError) "sseError" "SseError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SseError.ToHxOnString()) "sse-error" "SseError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SseOpen" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SseOpen) "sseOpen" "SseOpen event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SseOpen.ToHxOnString()) "sse-open" "SseOpen hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SwapError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SwapError) "swapError" "SwapError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SwapError.ToHxOnString()) "swap-error" "SwapError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "TargetError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string TargetError) "targetError" "TargetError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (TargetError.ToHxOnString()) "target-error" "TargetError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "Timeout" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string Timeout) "timeout" "Timeout event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (Timeout.ToHxOnString()) "timeout" "Timeout hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "ValidationValidate" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string ValidationValidate) "validation:validate" "ValidationValidate event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(ValidationValidate.ToHxOnString())
|
||||
"validation:validate"
|
||||
"ValidationValidate hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "ValidationFailed" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string ValidationFailed) "validation:failed" "ValidationFailed event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(ValidationFailed.ToHxOnString())
|
||||
"validation:failed"
|
||||
"ValidationFailed hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "ValidationHalted" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string ValidationHalted) "validation:halted" "ValidationHalted event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(ValidationHalted.ToHxOnString())
|
||||
"validation:halted"
|
||||
"ValidationHalted hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "XhrAbort" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string XhrAbort) "xhr:abort" "XhrAbort event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (XhrAbort.ToHxOnString()) "xhr:abort" "XhrAbort hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "XhrLoadEnd" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string XhrLoadEnd) "xhr:loadend" "XhrLoadEnd event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (XhrLoadEnd.ToHxOnString()) "xhr:loadend" "XhrLoadEnd hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "XhrLoadStart" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string XhrLoadStart) "xhr:loadstart" "XhrLoadStart event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (XhrLoadStart.ToHxOnString()) "xhr:loadstart" "XhrLoadStart hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "XhrProgress" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string XhrProgress) "xhr:progress" "XhrProgress event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (XhrProgress.ToHxOnString()) "xhr:progress" "XhrProgress hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
]
|
||||
@@ -282,6 +423,80 @@ let hxHeaders =
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxParams module
|
||||
let hxParams =
|
||||
testList "HxParams" [
|
||||
test "All is correct" {
|
||||
Expect.equal HxParams.All "*" "All is not correct"
|
||||
}
|
||||
test "None is correct" {
|
||||
Expect.equal HxParams.None "none" "None is not correct"
|
||||
}
|
||||
testList "With" [
|
||||
test "succeeds with empty list" {
|
||||
Expect.equal (HxParams.With []) "" "With with empty list should have been blank"
|
||||
}
|
||||
test "succeeds with one list item" {
|
||||
Expect.equal (HxParams.With [ "boo" ]) "boo" "With single item incorrect"
|
||||
}
|
||||
test "succeeds with multiple list items" {
|
||||
Expect.equal (HxParams.With [ "foo"; "bar"; "baz" ]) "foo,bar,baz" "With multiple items incorrect"
|
||||
}
|
||||
]
|
||||
testList "Except" [
|
||||
test "succeeds with empty list" {
|
||||
Expect.equal (HxParams.Except []) "not " "Except with empty list incorrect"
|
||||
}
|
||||
test "succeeds with one list item" {
|
||||
Expect.equal (HxParams.Except [ "that" ]) "not that" "Except single item incorrect"
|
||||
}
|
||||
test "succeeds with multiple list items" {
|
||||
Expect.equal (HxParams.Except [ "blue"; "green" ]) "not blue,green" "Except multiple items incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxRequest module
|
||||
let hxRequest =
|
||||
testList "HxRequest" [
|
||||
testList "Configure" [
|
||||
test "succeeds with an empty list" {
|
||||
Expect.equal (HxRequest.Configure []) "{ }" "Configure with empty list incorrect"
|
||||
}
|
||||
test "succeeds with a non-empty list" {
|
||||
Expect.equal
|
||||
(HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) """{ "a": "b", "c": "d" }"""
|
||||
"Configure with a non-empty list incorrect"
|
||||
}
|
||||
test "succeeds with all known params configured" {
|
||||
Expect.equal
|
||||
(HxRequest.Configure
|
||||
[ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
|
||||
"""{ "timeout": 1000, "credentials": false, "noHeaders": true }"""
|
||||
"Configure with all known params incorrect"
|
||||
}
|
||||
]
|
||||
test "Timeout succeeds" {
|
||||
Expect.equal (HxRequest.Timeout 50) "\"timeout\": 50" "Timeout value incorrect"
|
||||
}
|
||||
testList "Credentials" [
|
||||
test "succeeds when set to true" {
|
||||
Expect.equal (HxRequest.Credentials true) "\"credentials\": true" "Credentials value incorrect"
|
||||
}
|
||||
test "succeeds when set to false" {
|
||||
Expect.equal (HxRequest.Credentials false) "\"credentials\": false" "Credentials value incorrect"
|
||||
}
|
||||
]
|
||||
testList "NoHeaders" [
|
||||
test "succeeds when set to true" {
|
||||
Expect.equal (HxRequest.NoHeaders true) "\"noHeaders\": true" "NoHeaders value incorrect"
|
||||
}
|
||||
test "succeeds when set to false" {
|
||||
Expect.equal (HxRequest.NoHeaders false) "\"noHeaders\": false" "NoHeaders value incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxSync module
|
||||
let hxSync =
|
||||
testList "HxSync" [
|
||||
@@ -508,16 +723,9 @@ let shouldRender expected node =
|
||||
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>"""
|
||||
}
|
||||
@@ -525,11 +733,20 @@ let attributes =
|
||||
span [ _hxDelete "/this-endpoint" ] [] |> shouldRender """<span hx-delete="/this-endpoint"></span>"""
|
||||
}
|
||||
test "_hxDisable succeeds" {
|
||||
p [ _hxDisable "#that" ] [] |> shouldRender """<p hx-disable="#that"></p>"""
|
||||
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
|
||||
}
|
||||
test "_hxDisabledElt succeeds" {
|
||||
button [ _hxDisabledElt "this" ] [] |> shouldRender """<button hx-disabled-elt="this"></button>"""
|
||||
}
|
||||
test "_hxDisinherit succeeds" {
|
||||
strong [ _hxDisinherit "*" ] [] |> shouldRender """<strong hx-disinherit="*"></strong>"""
|
||||
}
|
||||
test "_hxEncoding succeeds" {
|
||||
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
|
||||
}
|
||||
test "_hxExt succeeds" {
|
||||
section [ _hxExt "extendme" ] [] |> shouldRender """<section hx-ext="extendme"></section>"""
|
||||
}
|
||||
test "_hxGet succeeds" {
|
||||
article [ _hxGet "/the-text" ] [] |> shouldRender """<article hx-get="/the-text"></article>"""
|
||||
}
|
||||
@@ -537,8 +754,11 @@ let attributes =
|
||||
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 "_hxHistory succeeds" {
|
||||
span [ _hxHistory false ] [] |> shouldRender """<span hx-history="false"></span>"""
|
||||
}
|
||||
test "_hxHistoryElt succeeds" {
|
||||
table [ _hxHistoryElt ] [] |> shouldRender """<table hx-history-elt></table>"""
|
||||
}
|
||||
test "_hxInclude succeeds" {
|
||||
a [ _hxInclude ".extra-stuff" ] [] |> shouldRender """<a hx-include=".extra-stuff"></a>"""
|
||||
@@ -546,10 +766,6 @@ let attributes =
|
||||
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>"""
|
||||
}
|
||||
@@ -558,7 +774,10 @@ let attributes =
|
||||
}
|
||||
test "_hxOnHxEvent succeeds" {
|
||||
strong [ _hxOnHxEvent BeforeSwap "changeStuff()" ] []
|
||||
|> shouldRender """<strong hx-on:htmx:before:swap="changeStuff()"></strong>"""
|
||||
|> shouldRender """<strong hx-on::before-swap="changeStuff()"></strong>"""
|
||||
}
|
||||
test "_hxParams succeeds" {
|
||||
br [ _hxParams "[p1,p2]" ] |> shouldRender """<br hx-params="[p1,p2]">"""
|
||||
}
|
||||
test "_hxPatch succeeds" {
|
||||
div [ _hxPatch "/arrrgh" ] [] |> shouldRender """<div hx-patch="/arrrgh"></div>"""
|
||||
@@ -582,24 +801,15 @@ let attributes =
|
||||
test "_hxReplaceUrl succeeds" {
|
||||
p [ _hxReplaceUrl "/something-else" ] [] |> shouldRender """<p hx-replace-url="/something-else"></p>"""
|
||||
}
|
||||
test "_hxRequest succeeds" {
|
||||
u [ _hxRequest "noHeaders" ] [] |> shouldRender """<u hx-request="noHeaders"></u>"""
|
||||
}
|
||||
test "_hxSelect succeeds" {
|
||||
nav [ _hxSelect "#navbar" ] [] |> shouldRender """<nav hx-select="#navbar"></nav>"""
|
||||
}
|
||||
test "_hxSelectOob succeeds" {
|
||||
section [ _hxSelectOob "#oob" ] [] |> shouldRender """<section hx-select-oob="#oob"></section>"""
|
||||
}
|
||||
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>"""
|
||||
}
|
||||
@@ -620,52 +830,19 @@ let attributes =
|
||||
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>"""
|
||||
test "_sseSwap succeeds" {
|
||||
ul [ _sseSwap "sseMessageName" ] [] |> shouldRender """<ul sse-swap="sseMessageName"></ul>"""
|
||||
}
|
||||
test "_sseConnect succeeds" {
|
||||
div [ _sseConnect "/gps/sse" ] [] |> shouldRender """<div sse-connect="/gps/sse"></div>"""
|
||||
}
|
||||
]
|
||||
|
||||
/// Tests for the HxTags module
|
||||
let hxTags =
|
||||
testList "HxTags" [
|
||||
test "hxPartial succeeds" {
|
||||
hxPartial [ _class "testy" ] [ str "Howdy" ]
|
||||
|> shouldRender """<hx-partial class="testy">Howdy</hx-partial>"""
|
||||
}
|
||||
]
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
/// Tests for the Script module
|
||||
let script =
|
||||
@@ -681,39 +858,16 @@ let script =
|
||||
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>"""
|
||||
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.min.js" integrity="sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V" crossorigin="anonymous"></script>"""
|
||||
"CDN minified script tag is incorrect"
|
||||
}
|
||||
test "cdnUnminified succeeds" {
|
||||
let html = RenderView.AsString.htmlNode Script.cdnUnminified
|
||||
Expect.equal
|
||||
html
|
||||
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-ESzWv77gBOAGtF3d7B8QiQ786cghRRsyhuYVVJGsrFGxwT0Dj1fLxReX9Ul6t4n9" crossorigin="anonymous"></script>"""
|
||||
$"""<script src="https://cdn.jsdelivr.net/npm/htmx.org@{HtmxVersion}/dist/htmx.js" integrity="sha384-Q+Dky3iHVJOr6wUjQ4ulh6uQ76an/t+ak1+PjMVaxRjbZamFLAG+u9InkfjbsEQf" crossorigin="anonymous"></script>"""
|
||||
"CDN unminified script tag is incorrect"
|
||||
}
|
||||
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
|
||||
@@ -869,12 +1023,12 @@ let allTests =
|
||||
hxEncoding
|
||||
hxEvent
|
||||
hxHeaders
|
||||
hxParams
|
||||
hxRequest
|
||||
hxSync
|
||||
hxTrigger
|
||||
hxVals
|
||||
attributes
|
||||
hxModifiers
|
||||
hxTags
|
||||
script
|
||||
renderFragment
|
||||
]
|
||||
|
||||
@@ -1,335 +0,0 @@
|
||||
module ViewEngineMax
|
||||
|
||||
open Expecto
|
||||
open Giraffe.ViewEngine.Htmax
|
||||
|
||||
/// Tests for the HxBrowserIndicatorConfigItem type
|
||||
let hxBrowserIndicatorConfigItem =
|
||||
testList "HxBrowserIndicatorConfigItem" [
|
||||
testList "ToHcon" [
|
||||
test "succeeds for BoostBrowserIndicator" {
|
||||
Expect.equal
|
||||
((BoostBrowserIndicator false).ToHcon())
|
||||
"boostBrowserIndicator:false"
|
||||
"The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxPreloadConfigItem type
|
||||
let hxPreloadConfigItem =
|
||||
testList "HxPreloadConfigItem" [
|
||||
testList "ToHcon" [
|
||||
test "succeeds for AutoBoost" {
|
||||
Expect.equal ((AutoBoost false).ToHcon()) "autoBoost:false" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for BoostEvent" {
|
||||
Expect.equal ((BoostEvent "mouseover").ToHcon()) "boostEvent:mouseover" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for BoostTimeout" {
|
||||
Expect.equal ((BoostTimeout 30_000).ToHcon()) "boostTimeout:30000" "The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxSseWsConfigItem type
|
||||
let hxSseWsConfigItem =
|
||||
testList "HxSseWsConfigItem" [
|
||||
testList "ToHcon" [
|
||||
test "succeeds for Reconnect" {
|
||||
Expect.equal ((Reconnect true).ToHcon()) "reconnect:true" "The HCON value was incorrect"
|
||||
}
|
||||
testList "ReconnectDelay" [
|
||||
test "succeeds for numeric value" {
|
||||
Expect.equal ((ReconnectDelay "3000").ToHcon()) "reconnectDelay:3000" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for value with units" {
|
||||
Expect.equal ((ReconnectDelay "3s").ToHcon()) "reconnectDelay:3s" "The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
testList "ReconnectMaxDelay" [
|
||||
test "succeeds for numeric value" {
|
||||
Expect.equal
|
||||
((ReconnectMaxDelay "30000").ToHcon()) "reconnectMaxDelay:30000" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for value with units" {
|
||||
Expect.equal
|
||||
((ReconnectMaxDelay "50s").ToHcon()) "reconnectMaxDelay:50s" "The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
testList "ReconnectMaxAttempts" [
|
||||
test "succeeds for negative 1" {
|
||||
Expect.equal
|
||||
((ReconnectMaxAttempts -1).ToHcon())
|
||||
"reconnectMaxAttempts:Infinity"
|
||||
"The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for zero" {
|
||||
Expect.equal
|
||||
((ReconnectMaxAttempts 0).ToHcon()) "reconnectMaxAttempts:0" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for positive number" {
|
||||
Expect.equal
|
||||
((ReconnectMaxAttempts 7).ToHcon()) "reconnectMaxAttempts:7" "The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
test "succeeds for ReconnectJitter" {
|
||||
Expect.equal ((ReconnectJitter 0.7).ToHcon()) "reconnectJitter:0.7" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for PauseOnBackground" {
|
||||
Expect.equal
|
||||
((PauseOnBackground false).ToHcon()) "pauseOnBackground:false" "The HCON value was incorrect"
|
||||
}
|
||||
test "succeeds for PendingRequestTtl" {
|
||||
Expect.equal ((PendingRequestTtl 5000).ToHcon()) "pendingRequestTTL:5000" "The HCON value was incorrect"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
/// Tests for the HxConfig module
|
||||
let hxConfig =
|
||||
testList "HxConfig" [
|
||||
test "hxBrowserIndicatorConfig succeeds" {
|
||||
Expect.equal
|
||||
(hxBrowserIndicatorConfig [ BoostBrowserIndicator true ])
|
||||
"browser-indicator.boostBrowserIndicator:true"
|
||||
"Config settings not correct"
|
||||
}
|
||||
test "hxPreloadConfig succeeds" {
|
||||
Expect.equal
|
||||
(hxPreloadConfig [ AutoBoost true; BoostTimeout 3_000 ])
|
||||
"preload.autoBoost:true preload.boostTimeout:3000"
|
||||
"Config settings not correct"
|
||||
}
|
||||
test "hxSseConfig succeeds" {
|
||||
Expect.equal
|
||||
(hxSseConfig [ ReconnectDelay "5s"; PauseOnBackground false ])
|
||||
"sse.reconnectDelay:5s sse.pauseOnBackground:false"
|
||||
"Config settings not correct"
|
||||
}
|
||||
test "hxWsConfig succeeds" {
|
||||
Expect.equal
|
||||
(hxWsConfig [ Reconnect false; PendingRequestTtl 40_000 ])
|
||||
"ws.reconnect:false ws.pendingRequestTTL:40000"
|
||||
"Config settings not correct"
|
||||
}
|
||||
]
|
||||
|
||||
/// Tests for the HxEvent module
|
||||
let hxEvent =
|
||||
testList "HxEvent" [
|
||||
testList "AfterSseConnection" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string AfterSseConnection) "afterSseConnection" "AfterSseConnection event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(AfterSseConnection.ToHxOnString())
|
||||
"after:sse:connection"
|
||||
"AfterSseConnection hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterSseMessage" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterWsConnection" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string AfterWsConnection) "afterWsConnection" "AfterWsConnection event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(AfterWsConnection.ToHxOnString())
|
||||
"after:ws:connection"
|
||||
"AfterWsConnection hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterWsMessage" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string AfterWsMessage) "afterWsMessage" "AfterWsMessage event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(AfterWsMessage.ToHxOnString()) "after:ws:message" "AfterWsMessage hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "AfterWsRequest" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string AfterWsRequest) "afterWsRequest" "AfterWsRequest event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(AfterWsRequest.ToHxOnString()) "after:ws:request" "AfterWsRequest hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeSseConnection" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string BeforeSseConnection) "beforeSseConnection" "BeforeSseConnection event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeSseConnection.ToHxOnString())
|
||||
"before:sse:connection"
|
||||
"BeforeSseConnection hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeSseMessage" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeSseMessage.ToHxOnString())
|
||||
"before:sse:message"
|
||||
"BeforeSseMessage hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeWsConnection" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal
|
||||
(string BeforeWsConnection) "beforeWsConnection" "BeforeWsConnection event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeWsConnection.ToHxOnString())
|
||||
"before:ws:connection"
|
||||
"BeforeWsConnection hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeWsMessage" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeWsMessage) "beforeWsMessage" "BeforeWsMessage event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeWsMessage.ToHxOnString()) "before:ws:message" "BeforeWsMessage hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "BeforeWsRequest" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string BeforeWsRequest) "beforeWsRequest" "BeforeWsRequest event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(BeforeWsRequest.ToHxOnString()) "before:ws:request" "BeforeWsRequest hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "DownloadComplete" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string DownloadComplete) "downloadComplete" "DownloadComplete event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(DownloadComplete.ToHxOnString())
|
||||
"download:complete"
|
||||
"DownloadComplete hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "DownloadProgress" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string DownloadProgress) "downloadProgress" "DownloadProgress event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(DownloadProgress.ToHxOnString())
|
||||
"download:progress"
|
||||
"DownloadProgress hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "DownloadStart" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string DownloadStart) "downloadStart" "DownloadStart event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal
|
||||
(DownloadStart.ToHxOnString()) "download:start" "DownloadStart hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SseClose" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SseClose) "sseClose" "SseClose event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SseClose.ToHxOnString()) "sse:close" "SseClose hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
testList "SseError" [
|
||||
test "ToString succeeds" {
|
||||
Expect.equal (string SseError) "sseError" "SseError event name not correct"
|
||||
}
|
||||
test "ToHxOnString succeeds" {
|
||||
Expect.equal (SseError.ToHxOnString()) "sse:error" "SseError hx-on event name not correct"
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
open Giraffe.ViewEngine
|
||||
|
||||
/// Pipe-able assertion for a rendered node
|
||||
let shouldRender expected node =
|
||||
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
|
||||
|
||||
/// Tests for the HtmaxAttrs module
|
||||
let attributes =
|
||||
testList "Attributes" [
|
||||
|
||||
test "_hxBrowserIndicator succeeds" {
|
||||
a [ _hxBrowserIndicator ] [] |> shouldRender """<a hx-browser-indicator="true"></a>"""
|
||||
}
|
||||
test "_hxLive succeeds" {
|
||||
main [ _hxLive "q().doStuff()" ] [] |> shouldRender """<main hx-live="q().doStuff()"></main>"""
|
||||
}
|
||||
test "_hxOnMax succeeds" {
|
||||
body [ _hxOnMax SseClose "alert(done)" ] []
|
||||
|> shouldRender """<body hx-on:htmx:sse:close="alert(done)"></body>"""
|
||||
}
|
||||
testList "_hxPreload" [
|
||||
test "succeeds when given an event" {
|
||||
blockquote [ _hxPreload (Some "focus") ] []
|
||||
|> shouldRender """<blockquote hx-preload="focus"></blockquote>"""
|
||||
}
|
||||
test "succeeds when not given an event" {
|
||||
em [ _hxPreload None ] [] |> shouldRender """<em hx-preload></em>"""
|
||||
}
|
||||
]
|
||||
test "_hxSseClose succeeds" {
|
||||
form [ _hxSseClose "fin" ] [] |> shouldRender """<form hx-sse:close="fin"></form>"""
|
||||
}
|
||||
test "_hxSseConnect succeeds" {
|
||||
div [ _hxSseConnect "/path/to/resource" ] []
|
||||
|> shouldRender """<div hx-sse:connect="/path/to/resource"></div>"""
|
||||
}
|
||||
test "_hxTargets succeeds" {
|
||||
dl [ _hxTargets ".ephemeral" ] [] |> shouldRender """<dl hx-targets=".ephemeral"></dl>"""
|
||||
}
|
||||
test "_hxWsConnect succeeds" {
|
||||
p [ _hxWsConnect "/over/here" ] [] |> shouldRender """<p hx-ws:connect="/over/here"></p>"""
|
||||
}
|
||||
testList "_hxWsSend" [
|
||||
test "succeeds when given a URL" {
|
||||
span [ _hxWsSend (Some "/a/new/place") ] []
|
||||
|> shouldRender """<span hx-ws:send="/a/new/place"></span>"""
|
||||
}
|
||||
test "succeeds when not given a URL" {
|
||||
aside [ _hxWsSend None ] [] |> shouldRender """<aside hx-ws:send></aside>"""
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
let allTests =
|
||||
testList "ViewEngine.Htmax" [
|
||||
hxBrowserIndicatorConfigItem
|
||||
hxPreloadConfigItem
|
||||
hxSseWsConfigItem
|
||||
hxConfig
|
||||
hxEvent
|
||||
attributes
|
||||
]
|
||||
@@ -9,14 +9,13 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Htmx.fs" />
|
||||
<Compile Include="Htmax.fs" />
|
||||
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||
<None Include="..\icon.png" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Giraffe.ViewEngine" />
|
||||
<PackageReference Include="FSharp.Core" />
|
||||
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
/// <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"
|
||||
+315
-258
@@ -1,10 +1,244 @@
|
||||
/// <summary>Types and functions supporting htmx attributes in Giraffe View Engine</summary>
|
||||
module Giraffe.ViewEngine.Htmx
|
||||
|
||||
/// <summary>Helpers to define <c>hx-config</c> attribute values</summary>
|
||||
/// <seealso href="https://four.htmx.org/attributes/hx-config/">Documentation</seealso>
|
||||
/// <summary>Valid values for the <c>hx-encoding</c> attribute</summary>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxConfig =
|
||||
module HxEncoding =
|
||||
|
||||
/// <summary>A standard HTTP form</summary>
|
||||
[<Literal>]
|
||||
let Form = "application/x-www-form-urlencoded"
|
||||
|
||||
/// <summary>A multipart form (used for file uploads)</summary>
|
||||
[<Literal>]
|
||||
let MultipartForm = "multipart/form-data"
|
||||
|
||||
|
||||
/// <summary>The events recognized by htmx</summary>
|
||||
[<Struct>]
|
||||
type HxEvent =
|
||||
|
||||
/// <summary>Send this event to an element to abort a request</summary>
|
||||
| Abort
|
||||
|
||||
/// <summary>Triggered after an AJAX request has completed processing a successful response</summary>
|
||||
| AfterOnLoad
|
||||
|
||||
/// <summary>Triggered after htmx has initialized a node</summary>
|
||||
| AfterProcessNode
|
||||
|
||||
/// <summary>Triggered after an AJAX request has completed</summary>
|
||||
| AfterRequest
|
||||
|
||||
/// <summary>Triggered after the DOM has settled</summary>
|
||||
| AfterSettle
|
||||
|
||||
/// <summary>Triggered after new content has been swapped in</summary>
|
||||
| AfterSwap
|
||||
|
||||
/// <summary>Triggered before htmx disables an element or removes it from the DOM</summary>
|
||||
| BeforeCleanupElement
|
||||
|
||||
/// <summary>Triggered before any response processing occurs</summary>
|
||||
| BeforeOnLoad
|
||||
|
||||
/// <summary>Triggered before htmx initializes a node</summary>
|
||||
| BeforeProcessNode
|
||||
|
||||
/// <summary>Triggered before an AJAX request is made</summary>
|
||||
| BeforeRequest
|
||||
|
||||
/// <summary>Triggered before a swap is done, allows you to configure the swap</summary>
|
||||
| BeforeSwap
|
||||
|
||||
/// <summary>Triggered just before an ajax request is sent</summary>
|
||||
| BeforeSend
|
||||
|
||||
/// <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 on an error during cache writing</summary>
|
||||
| HistoryCacheError
|
||||
|
||||
/// <summary>Triggered on a cache miss in the history subsystem</summary>
|
||||
| HistoryCacheMiss
|
||||
|
||||
/// <summary>Triggered on a unsuccessful remote retrieval</summary>
|
||||
| HistoryCacheMissError
|
||||
|
||||
/// <summary>Triggered on a successful remote retrieval</summary>
|
||||
| HistoryCacheMissLoad
|
||||
|
||||
/// <summary>Triggered when htmx handles a history restoration action</summary>
|
||||
| HistoryRestore
|
||||
|
||||
/// <summary>Triggered before content is saved to the history cache</summary>
|
||||
| BeforeHistorySave
|
||||
|
||||
/// <summary>Triggered when new content is added to the DOM</summary>
|
||||
| Load
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined
|
||||
/// </summary>
|
||||
| NoSseSourceError
|
||||
|
||||
/// <summary>Triggered when an exception occurs during the onLoad handling in htmx</summary>
|
||||
| OnLoadError
|
||||
|
||||
/// <summary>Triggered after an out of band element as been swapped in</summary>
|
||||
| OobAfterSwap
|
||||
|
||||
/// <summary>Triggered before an out of band element swap is done, allows you to configure the swap</summary>
|
||||
| OobBeforeSwap
|
||||
|
||||
/// <summary>Triggered when an out of band element does not have a matching ID in the current DOM</summary>
|
||||
| OobErrorNoTarget
|
||||
|
||||
/// <summary>Triggered after a prompt is shown</summary>
|
||||
| Prompt
|
||||
|
||||
/// <summary>Triggered after an url is pushed into history</summary>
|
||||
| PushedIntoHistory
|
||||
|
||||
/// <summary>Triggered when an HTTP response error (non-200 or 300 response code) occurs</summary>
|
||||
| ResponseError
|
||||
|
||||
/// <summary>Triggered when a network error prevents an HTTP request from happening</summary>
|
||||
| SendError
|
||||
|
||||
/// <summary>Triggered when an error occurs with a SSE source</summary>
|
||||
| SseError
|
||||
|
||||
/// <summary>Triggered when a SSE source is opened</summary>
|
||||
| SseOpen
|
||||
|
||||
/// <summary>Triggered when an error occurs during the swap phase</summary>
|
||||
| SwapError
|
||||
|
||||
/// <summary>Triggered when an invalid target is specified</summary>
|
||||
| TargetError
|
||||
|
||||
/// <summary>Triggered when a request timeout occurs</summary>
|
||||
| Timeout
|
||||
|
||||
/// <summary>Triggered before an element is validated</summary>
|
||||
| ValidationValidate
|
||||
|
||||
/// <summary>Triggered when an element fails validation</summary>
|
||||
| ValidationFailed
|
||||
|
||||
/// <summary>Triggered when a request is halted due to validation errors</summary>
|
||||
| ValidationHalted
|
||||
|
||||
/// <summary>Triggered when an ajax request aborts</summary>
|
||||
| XhrAbort
|
||||
|
||||
/// <summary>Triggered when an ajax request ends</summary>
|
||||
| XhrLoadEnd
|
||||
|
||||
/// <summary>Triggered when an ajax request starts</summary>
|
||||
| XhrLoadStart
|
||||
|
||||
/// <summary>Triggered periodically during an ajax request that supports progress events</summary>
|
||||
| XhrProgress
|
||||
|
||||
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
|
||||
static member private Values = Map [
|
||||
Abort, ("abort", "abort")
|
||||
AfterOnLoad, ("afterOnLoad", "after-on-load")
|
||||
AfterProcessNode, ("afterProcessNode", "after-process-node")
|
||||
AfterRequest, ("afterRequest", "after-request")
|
||||
AfterSettle, ("afterSettle", "after-settle")
|
||||
AfterSwap, ("afterSwap", "after-swap")
|
||||
BeforeCleanupElement, ("beforeCleanupElement", "before-cleanup-element")
|
||||
BeforeOnLoad, ("beforeOnLoad", "before-on-load")
|
||||
BeforeProcessNode, ("beforeProcessNode", "before-process-node")
|
||||
BeforeRequest, ("beforeRequest", "before-request")
|
||||
BeforeSwap, ("beforeSwap", "before-swap")
|
||||
BeforeSend, ("beforeSend", "before-send")
|
||||
ConfigRequest, ("configRequest", "config-request")
|
||||
Confirm, ("confirm", "confirm")
|
||||
HistoryCacheError, ("historyCacheError", "history-cache-error")
|
||||
HistoryCacheMiss, ("historyCacheMiss", "history-cache-miss")
|
||||
HistoryCacheMissError, ("historyCacheMissError", "history-cache-miss-error")
|
||||
HistoryCacheMissLoad, ("historyCacheMissLoad", "history-cache-miss-load")
|
||||
HistoryRestore, ("historyRestore", "history-restore")
|
||||
BeforeHistorySave, ("beforeHistorySave", "before-history-save")
|
||||
Load, ("load", "load")
|
||||
NoSseSourceError, ("noSSESourceError", "no-sse-source-error")
|
||||
OnLoadError, ("onLoadError", "on-load-error")
|
||||
OobAfterSwap, ("oobAfterSwap", "oob-after-swap")
|
||||
OobBeforeSwap, ("oobBeforeSwap", "oob-before-swap")
|
||||
OobErrorNoTarget, ("oobErrorNoTarget", "oob-error-no-target")
|
||||
Prompt, ("prompt", "prompt")
|
||||
PushedIntoHistory, ("pushedIntoHistory", "pushed-into-history")
|
||||
ResponseError, ("responseError", "response-error")
|
||||
SendError, ("sendError", "send-error")
|
||||
SseError, ("sseError", "sse-error")
|
||||
SseOpen, ("sseOpen", "sse-open")
|
||||
SwapError, ("swapError", "swap-error")
|
||||
TargetError, ("targetError", "target-error")
|
||||
Timeout, ("timeout", "timeout")
|
||||
ValidationValidate, ("validation:validate", "validation:validate")
|
||||
ValidationFailed, ("validation:failed", "validation:failed")
|
||||
ValidationHalted, ("validation:halted", "validation:halted")
|
||||
XhrAbort, ("xhr:abort", "xhr:abort")
|
||||
XhrLoadEnd, ("xhr:loadend", "xhr:loadend")
|
||||
XhrLoadStart, ("xhr:loadstart", "xhr:loadstart")
|
||||
XhrProgress, ("xhr:progress", "xhr:progress")
|
||||
]
|
||||
|
||||
/// <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>]
|
||||
module HxHeaders =
|
||||
|
||||
/// <summary>Create headers from a list of key/value pairs</summary>
|
||||
let From = Giraffe.Htmx.Common.toJson
|
||||
|
||||
|
||||
/// <summary>Values / helpers for the <c>hx-params</c> attribute</summary>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxParams =
|
||||
|
||||
/// <summary>Include all parameters</summary>
|
||||
[<Literal>]
|
||||
let All = "*"
|
||||
|
||||
/// <summary>Include no parameters</summary>
|
||||
[<Literal>]
|
||||
let None = "none"
|
||||
|
||||
/// <summary>Include the specified parameters</summary>
|
||||
/// <param name="fields">One or more fields to include in the request</param>
|
||||
/// <returns>The list of fields for the <c>hx-params</c> attribute value</returns>
|
||||
let With fields =
|
||||
match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}")
|
||||
|
||||
/// <summary>Exclude the specified parameters</summary>
|
||||
/// <param name="fields">One or more fields to exclude from the request</param>
|
||||
/// <returns>The list of fields for the <c>hx-params</c> attribute value prefixed with "not"</returns>
|
||||
let Except fields =
|
||||
With fields |> sprintf "not %s"
|
||||
|
||||
|
||||
/// <summary>Helpers to define <c>hx-request</c> attribute values</summary>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxRequest =
|
||||
|
||||
open Giraffe.Htmx.Common
|
||||
|
||||
@@ -37,144 +271,6 @@ module HxConfig =
|
||||
(toLowerBool >> sprintf "\"noHeaders\": %s") exclude
|
||||
|
||||
|
||||
/// <summary>Valid values for the <c>hx-encoding</c> attribute</summary>
|
||||
[<RequireQualifiedAccess>]
|
||||
module HxEncoding =
|
||||
|
||||
/// <summary>A standard HTTP form</summary>
|
||||
[<Literal>]
|
||||
let Form = "application/x-www-form-urlencoded"
|
||||
|
||||
/// <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>]
|
||||
module HxHeaders =
|
||||
|
||||
/// <summary>Create headers from a list of key/value pairs</summary>
|
||||
let From = Giraffe.Htmx.Common.toJson
|
||||
|
||||
|
||||
/// <summary>Helpers for the <c>hx-sync</c> attribute</summary>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
|
||||
[<RequireQualifiedAccess>]
|
||||
@@ -391,31 +487,16 @@ module HxVals =
|
||||
let From = Giraffe.Htmx.Common.toJson
|
||||
|
||||
|
||||
open System.Net.Http
|
||||
open Giraffe.Htmx
|
||||
|
||||
/// <summary>Attributes and flags for htmx</summary>
|
||||
[<AutoOpen>]
|
||||
module HtmxAttrs =
|
||||
|
||||
/// <summary>The URL which will receive the request (use with <c>_hxMethod</c>)</summary>
|
||||
/// <param name="url">The URL for the attribute</param>
|
||||
/// <seealso href="https://four.htmx.org/attributes/hx-action/">Documentation</seealso>
|
||||
let _hxAction url =
|
||||
attr "hx-action" url
|
||||
|
||||
/// <summary>Progressively enhances anchors and forms to use AJAX requests</summary>
|
||||
/// <remarks>Use <c>_hxNoBoost</c> to set to false</remarks>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-boost/">Documentation</seealso>
|
||||
let _hxBoost =
|
||||
attr "hx-boost" "true"
|
||||
|
||||
/// <summary>Configure request behavior</summary>
|
||||
/// <param name="config">The configuration parameters to use for the request</param>
|
||||
/// <returns>A configured <c>hx-config</c> attribute</returns>
|
||||
/// <seealso cref="https://four.htmx.org/attributes/hx-config/">Documentation</seealso>
|
||||
let _hxConfig config =
|
||||
attr "hx-config" (toJson config)
|
||||
let _hxBoost = attr "hx-boost" "true"
|
||||
|
||||
/// <summary>Shows a <c>confirm()</c> dialog before issuing a request</summary>
|
||||
/// <param name="prompt">The prompt to present to the user when seeking their confirmation</param>
|
||||
@@ -431,17 +512,23 @@ module HtmxAttrs =
|
||||
let _hxDelete url =
|
||||
attr "hx-delete" url
|
||||
|
||||
/// <summary>Disables htmx processing for the given node and any children nodes</summary>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-disable/">Documentation</seealso>
|
||||
let _hxDisable = flag "hx-disable"
|
||||
|
||||
/// <summary>Specifies elements that should be disabled when an htmx request is in flight</summary>
|
||||
/// <param name="elt">The element to disable when an htmx request is in flight</param>
|
||||
/// <returns>A configured <c>hx-disable</c> attribute</returns>
|
||||
/// <remarks>This behavior changed in v4
|
||||
/// <ul><li>For the v2 behavior of <c>hx-disable</c> (which will manifest as compile errors), change instances to
|
||||
/// <c>_hxIgnore</c></li>
|
||||
/// <li>For v4 behavior, changing <c>_hxDisabledElt</c> to <c>_hxDisable</c> should be sufficient</li></ul>
|
||||
/// </remarks>
|
||||
/// <seealso href="https://four.htmx.org/attributes/hx-disable/">Documentation</seealso>
|
||||
let _hxDisable elt =
|
||||
attr "hx-disable" elt
|
||||
/// <returns>A configured <c>hx-disabled-elt</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-disabled-elt/">Documentation</seealso>
|
||||
let _hxDisabledElt elt =
|
||||
attr "hx-disabled-elt" elt
|
||||
|
||||
/// <summary>Disinherit all ("*") or specific htmx attributes</summary>
|
||||
/// <param name="hxAttrs">The htmx attributes to disinherit (should start with "hx-")</param>
|
||||
/// <returns>A configured <c>hx-disinherit</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-disinherit/">Documentation</seealso>
|
||||
let _hxDisinherit hxAttrs =
|
||||
attr "hx-disinherit" hxAttrs
|
||||
|
||||
/// <summary>Changes the request encoding type</summary>
|
||||
/// <param name="enc">The encoding type (use <c>HxEncoding</c> constants)</param>
|
||||
@@ -451,6 +538,13 @@ module HtmxAttrs =
|
||||
let _hxEncoding enc =
|
||||
attr "hx-encoding" enc
|
||||
|
||||
/// <summary>Extensions to use for this element</summary>
|
||||
/// <param name="exts">A list of extensions to apply to this element</param>
|
||||
/// <returns>A configured <c>hx-ext</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-ext/">Documentation</seealso>
|
||||
let _hxExt exts =
|
||||
attr "hx-ext" exts
|
||||
|
||||
/// <summary>Issues a <c>GET</c> to the specified URL</summary>
|
||||
/// <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>
|
||||
@@ -465,10 +559,19 @@ module HtmxAttrs =
|
||||
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>
|
||||
/// Set to "false" to prevent pages with sensitive information from being stored in the history cache
|
||||
/// </summary>
|
||||
/// <param name="shouldStore">Whether the page should be stored in the history cache</param>
|
||||
/// <returns>A configured <c>hx-history</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-history/">Documentation</seealso>
|
||||
let _hxHistory shouldStore =
|
||||
attr "hx-history" (toLowerBool shouldStore)
|
||||
|
||||
/// <summary>The element to snapshot and restore during history navigation</summary>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-history-elt/">Documentation</seealso>
|
||||
let _hxHistoryElt =
|
||||
flag "hx-history-elt"
|
||||
|
||||
/// <summary>Includes additional data in AJAX requests</summary>
|
||||
/// <param name="spec">The specification of what should be included in the request</param>
|
||||
@@ -484,12 +587,6 @@ module HtmxAttrs =
|
||||
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 =
|
||||
@@ -506,12 +603,19 @@ module HtmxAttrs =
|
||||
/// <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>
|
||||
/// <returns>A configured <c>hx-on::</c> attribute</returns>
|
||||
/// <seealso cref="HxEvent" />
|
||||
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
|
||||
let _hxOnHxEvent (hxEvent: HxEvent) handler =
|
||||
_hxOnEvent $"htmx:{hxEvent.ToHxOnString()}" handler
|
||||
_hxOnEvent $":{hxEvent.ToHxOnString()}" handler
|
||||
|
||||
/// <summary>Filters the parameters that will be submitted with a request</summary>
|
||||
/// <param name="toInclude">The fields to include (use <c>HxParams</c> to generate this value)</param>
|
||||
/// <returns>A configured <c>hx-params</c> attribute</returns>
|
||||
/// <seealso cref="HxParams" />
|
||||
/// <seealso href="https://htmx.org/attributes/hx-params/">Documentation</seealso>
|
||||
let _hxParams toInclude =
|
||||
attr "hx-params" toInclude
|
||||
|
||||
/// <summary>Issues a <c>PATCH</c> to the specified URL</summary>
|
||||
/// <param name="url">The URL to which the request should be directed</param>
|
||||
@@ -535,12 +639,7 @@ module HtmxAttrs =
|
||||
/// <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>
|
||||
/// <remarks>The value provided will be in the <c>HX-Prompt</c> request header</remarks>
|
||||
/// <seealso href="https://htmx.org/attributes/hx-prompt/">Documentation</seealso>
|
||||
let _hxPrompt text =
|
||||
attr "hx-prompt" text
|
||||
@@ -578,6 +677,14 @@ module HtmxAttrs =
|
||||
let _hxReplaceUrl spec =
|
||||
attr "hx-replace-url" spec
|
||||
|
||||
/// <summary>Configures various aspects of the request</summary>
|
||||
/// <param name="spec">The configuration spec (use <c>HxRequest.Configure</c> to create value)</param>
|
||||
/// <returns>A configured <c>hx-request</c> attribute</returns>
|
||||
/// <seealso cref="HxRequest.Configure" />
|
||||
/// <seealso href="https://htmx.org/attributes/hx-request/">Documentation</seealso>
|
||||
let _hxRequest spec =
|
||||
attr "hx-request" spec
|
||||
|
||||
/// <summary>Selects a subset of the server response to process</summary>
|
||||
/// <param name="selector">A CSS selector for the content to be selected</param>
|
||||
/// <returns>A configured <c>hx-select</c> attribute</returns>
|
||||
@@ -592,31 +699,6 @@ module HtmxAttrs =
|
||||
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>
|
||||
@@ -688,75 +770,50 @@ module HtmxAttrs =
|
||||
let _hxVals values =
|
||||
attr "hx-vals" values
|
||||
|
||||
/// <summary>The URL of the SSE server</summary>
|
||||
/// <param name="url">The URL from which events will be received</param>
|
||||
/// <returns>A configured <c>sse-connect</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
|
||||
let _sseConnect url =
|
||||
attr "sse-connect" url
|
||||
|
||||
/// <summary>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>The name(s) of the message(s) to swap into the DOM</summary>
|
||||
/// <param name="messages">The message names (comma-delimited) to swap (use "message" for unnamed events)</param>
|
||||
/// <returns>A configured <c>sse-swap</c> attribute</returns>
|
||||
/// <seealso href="https://htmx.org/extensions/sse/#usage">Extension Docs</seealso>
|
||||
let _sseSwap messages =
|
||||
attr "sse-swap" messages
|
||||
|
||||
|
||||
/// <summary>Script tags to pull htmx into a web page</summary>
|
||||
module Script =
|
||||
|
||||
open System
|
||||
|
||||
/// <summary>Script tag to load the package-provided version of htmx</summary>
|
||||
let local = script [ _src StaticAssetUrl.htmx ] []
|
||||
let local = script [ _src htmxLocalScript ] []
|
||||
|
||||
/// <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"
|
||||
_integrity "sha384-H5SrcfygHmAuTDZphMHqBJLc3FhssKjG7w/CeCpFReSfwBWDTKpkzPP8c+cLsK+V"
|
||||
_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"
|
||||
_integrity "sha384-Q+Dky3iHVJOr6wUjQ4ulh6uQ76an/t+ak1+PjMVaxRjbZamFLAG+u9InkfjbsEQf"
|
||||
_crossorigin "anonymous" ] []
|
||||
|
||||
/// <summary>Script tags to load the htmax bundle</summary>
|
||||
module Max =
|
||||
/// <summary>Script tag to load the minified version from jsdelivr.net</summary>
|
||||
[<Obsolete "Use cdnMinified instead; this will be removed in v4">]
|
||||
let minified = cdnMinified
|
||||
|
||||
/// <summary>Script tag to load the 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>Script tag to load the unminified version from jsdelivr.net</summary>
|
||||
[<Obsolete "Use cdnUnminified instead; this will be removed in v4">]
|
||||
let unminified = cdnUnminified
|
||||
|
||||
|
||||
/// <summary>Functions to extract and render an HTML fragment from a document</summary>
|
||||
|
||||
@@ -2,19 +2,14 @@
|
||||
|
||||
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
|
||||
|
||||
**htmx version: 4.0.0**
|
||||
**htmx version: 2.0.10**
|
||||
|
||||
_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._
|
||||
_Upgrading from v1.x: see [the migration guide](https://htmx.org/migration-guide-htmx-1/) for changes_
|
||||
|
||||
### 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`.
|
||||
2. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`.
|
||||
|
||||
### Use
|
||||
|
||||
@@ -26,16 +21,15 @@ let autoload =
|
||||
```
|
||||
|
||||
Support modules include:
|
||||
- `HxConfig` _(new in v4, replaces `HxRequest`)_
|
||||
- `HxEncoding`
|
||||
- `HxHeaders`
|
||||
- `HxParams`
|
||||
- `HxRequest`
|
||||
- `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`._
|
||||
`Htmx.Script.local` creates an `XmlNode` to load the package-provided htmx library. There are also two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.cdnMinified` loads the minified version, and `Htmx.Script.cdnUnminified` loads the unminified version (useful for debugging). When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`.
|
||||
|
||||
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).
|
||||
|
||||
@@ -48,10 +42,10 @@ htmx's attributes and these attribute functions map one-to-one. There are two ex
|
||||
The htmx `hx-on` attribute supports multiple events if they are separated with a newline (`\n`) character. The value provided to this attribute will be attribute-escaped, but in testing, it was interpreted correctly.
|
||||
|
||||
The 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.
|
||||
- `HxRequest` 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
|
||||
HxRequest.Configure [ HxRequest.Timeout 500 ] |> _hxRequest
|
||||
```
|
||||
- `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user