Compare commits

...

30 Commits

Author SHA1 Message Date
danieljsummers 5c12ad7cef v4, beta4 (#19)
- Update with header/attribute changes between alpha8 and beta4
- Add support for htmax script, attributes, headers, and extension config

Reviewed-on: #19
2026-06-15 01:07:38 +00:00
danieljsummers d4a7e0c9ce htmx v4, alpha8 (#18)
Reviewed-on: #18
2026-03-22 22:21:22 +00:00
danieljsummers de75672bb3 v4, alpha 6 (#17)
Reviewed-on: #17
2026-01-07 11:37:26 +00:00
danieljsummers 121eb95d87 v2.0.8 (#16)
- Embed htmx library to `Giraffe.Htmx.Common`, add links to load package-provided script
- Update CDN links for v2.0.8
- Add .NET 10 support

Reviewed-on: #16
2025-12-28 16:53:44 +00:00
danieljsummers 6b7458070b v2.0.6 (#15)
- Adds XML documentation (#13)
- Adds `HxSync` module and attribute helper
- Updates script version to 2.0.6
- Drops .NET 6 support (#14 )

Reviewed-on: #15
2025-07-03 00:15:24 +00:00
danieljsummers 10c31d77b5 Update version in READMEs 2024-12-13 17:02:28 -05:00
danieljsummers f4cd184a15 Update script and version to 2.0.4; add .NET 9 support 2024-12-13 16:57:27 -05:00
danieljsummers 5626031593 Update script tags and package version to 2.0.3 2024-10-18 09:36:28 -04:00
danieljsummers f0de18845f Update script tags and package version to 2.0.2 2024-08-12 21:36:47 -04:00
danieljsummers 961307fd99 Update htmx script to 2.0.1 2024-07-29 18:06:19 -04:00
danieljsummers a2960a79c6 Add pack script; drop beta tag 2024-06-18 19:38:28 -04:00
danieljsummers 541384a92f htmx v2
- Drop `hx-sse` and `hx-ws` attributes
- Drop explicit .NET 7 support
- Update deps to latest versions
2024-06-18 19:16:10 -04:00
danieljsummers 8cb5d6bfa7 Merge pull request 'htmx v2-beta4' (#12) from v2-beta4 into main
Reviewed-on: #12
2024-05-23 23:30:58 +00:00
danieljsummers 1a11e3511a Update htmx to v2.0.0-beta4 2024-05-23 19:26:00 -04:00
danieljsummers 32e962416d Migrate PR 10 from GitHub
https://github.com/bit-badger/Giraffe.Htmx/pull/10
2024-05-23 19:19:07 -04:00
danieljsummers 29839fa795 Update script tags for v2.0.0 beta3
- Update paths in package metadata
2024-04-17 22:25:06 -04:00
danieljsummers 7f9b3a6234 Merge branch 'htmx-version-2' 2024-04-17 22:06:23 -04:00
danieljsummers a8d2b819dc Merge branch 'htmx-version-2' of https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx into htmx-version-2 2024-04-17 22:04:21 -04:00
danieljsummers 1ea05b79ed Update for htmx v1.9.11 2024-03-18 20:14:51 -04:00
danieljsummers 4f6bb8367a Update for v2 beta 1 2024-03-18 20:03:18 -04:00
danieljsummers b3665a4b72 Update CI to Node 20-based versions 2024-02-12 18:29:27 -05:00
danieljsummers bdb7255a1c Update for v2 alpha 2 2024-02-12 17:45:16 -05:00
danieljsummers 9276db7ffe Sync changes from main 2024-01-30 23:04:19 -05:00
danieljsummers 94b68f76c9 Add workflow dispatch to CI 2024-01-30 23:02:45 -05:00
danieljsummers 90de16529c Move packaging to .NET 8 2024-01-30 22:58:45 -05:00
danieljsummers 452f15b2d4 Add alpha tag 2024-01-30 22:55:36 -05:00
danieljsummers 59246ae7f5 Add v2 branch to CI 2024-01-30 22:48:08 -05:00
danieljsummers 16355e8f58 Add v2.0.0-alpha1 support 2024-01-30 22:46:34 -05:00
danieljsummers 71286b9064 Move FSharp.Core version to .fsproj files
- Change event DU to struct
2024-01-02 19:26:09 -05:00
danieljsummers 85ac22877c v1.9.10 (#9) 2024-01-02 19:02:24 -05:00
27 changed files with 3235 additions and 678 deletions
+8 -7
View File
@@ -2,9 +2,10 @@ name: CI
on: on:
push: push:
branches: [ "main" ] branches: [ "main", "htmx-version-2" ]
pull_request: pull_request:
branches: [ "main" ] branches: [ "main" ]
workflow_dispatch:
jobs: jobs:
build-and-test: build-and-test:
@@ -16,9 +17,9 @@ jobs:
dotnet-version: [ "6.0", "7.0", "8.0" ] dotnet-version: [ "6.0", "7.0", "8.0" ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup .NET ${{ matrix.dotnet-version }}.x - name: Setup .NET ${{ matrix.dotnet-version }}.x
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: ${{ matrix.dotnet-version }}.x dotnet-version: ${{ matrix.dotnet-version }}.x
- name: Restore dependencies - name: Restore dependencies
@@ -31,11 +32,11 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: build-and-test needs: build-and-test
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v4
- name: Setup .NET - name: Setup .NET
uses: actions/setup-dotnet@v3 uses: actions/setup-dotnet@v4
with: with:
dotnet-version: "7.0" dotnet-version: "8.0"
- name: Package Common library - name: Package Common library
run: dotnet pack src/Common/Giraffe.Htmx.Common.fsproj -c Release run: dotnet pack src/Common/Giraffe.Htmx.Common.fsproj -c Release
- name: Move Common package - name: Move Common package
@@ -49,7 +50,7 @@ jobs:
- name: Move View Engine package - name: Move View Engine package
run: cp src/ViewEngine.Htmx/bin/Release/Giraffe.ViewEngine.Htmx.*.nupkg . run: cp src/ViewEngine.Htmx/bin/Release/Giraffe.ViewEngine.Htmx.*.nupkg .
- name: Save Packages - name: Save Packages
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: packages name: packages
path: | path: |
+3
View File
@@ -6,3 +6,6 @@
.idea .idea
*.user *.user
.vscode .vscode
src/*.nupkg
src/tests*.txt
+22 -15
View File
@@ -14,6 +14,8 @@ htmx uses attributes and HTTP headers to attain its interactivity; the libraries
|---|---| |---|---|
|[![Nuget](https://img.shields.io/nuget/v/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/)|[![Nuget](https://img.shields.io/nuget/v/Giraffe.ViewEngine.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)| |[![Nuget](https://img.shields.io/nuget/v/Giraffe.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.Htmx/)|[![Nuget](https://img.shields.io/nuget/v/Giraffe.ViewEngine.Htmx?style=plastic)](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx/)|
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`) ## Server Side (`Giraffe.Htmx`)
In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://htmx.org/docs/#request_headers) along with the request. Once `Giraffe.Htmx` is opened, these are available as properties on `HttpContext.Request.Headers`. These consist of the header name, translated to a .NET name (ex. `HX-Current-URL` becomes `HxCurrentUrl`), and a strongly-typed property based on the expected value of that header. Additionally, they are all exposed as `Option`s, as they may or may not be present for any given request. In addition to the regular HTTP request payloads, htmx sets [one or more headers](https://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.
@@ -21,19 +23,20 @@ In addition to the regular HTTP request payloads, htmx sets [one or more headers
A server may want to respond to a request that originated from htmx differently than a regular request. One way htmx can provide the same feel as a Single Page Application (SPA) is by swapping out the `body` content (or an element within it) instead of reloading the entire page. In this case, the developer can provide a partial layout to be used for these responses, while returning the full page for regular requests. The `IsHtmx` property makes this easy... A server may want to respond to a request that originated from htmx differently than a regular request. One way htmx can provide the same feel as a Single Page Application (SPA) is by swapping out the `body` content (or an element within it) instead of reloading the entire page. In this case, the developer can provide a partial layout to be used for these responses, while returning the full page for regular requests. The `IsHtmx` property makes this easy...
```fsharp ```fsharp
// "partial" and "full" are handlers that return the contents; // "partial" and "full" are handlers that return the contents;
// "view" can be whatever your view engine needs for the body of the page // "view" can be whatever your view engine needs for the body of the page
let result view : HttpHandler = let result view : HttpHandler =
fun next ctx -> fun next ctx ->
match ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh with if ctx.Request.IsHtmx && not ctx.Request.IsHtmxRefresh then
| true -> partial view partial view
| false -> full view else
full view
``` ```
htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response. htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) to affect client-side behavior. For each of these, this library provides `HttpHandler`s that can be chained along with the response. As an example, if the server returns a redirect response (301, 302, 303, 307), the `XMLHttpRequest` handler on the client will follow the redirection before htmx can do anything with it. To redirect to a new page, you would return an OK (200) response with an `HX-Redirect` header set in the response.
```fsharp ```fsharp
let theHandler : HttpHandler = let theHandler : HttpHandler =
fun next ctx -> fun next ctx ->
// some interesting stuff // some interesting stuff
withHxRedirect "/the-new-url" >=> Successful.OK withHxRedirect "/the-new-url" >=> Successful.OK
@@ -41,6 +44,8 @@ htmx also utilizes [response headers](https://htmx.org/docs/#response_headers) t
Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well. Of note is that the `HX-Trigger` headers can take either one or more events. For a single event with no parameters, use `withHxTrigger`; for a single event with parameters, or multiple events, use `withHxTriggerMany`. Both these have `AfterSettle` and `AfterSwap` versions as well.
`HtmxScript.local` provides an `HtmlString` with a script tag to load the package-provided htmx library. This can be used in code, Razor templates, etc. (If you're using Giraffe.ViewEngine, see below.)
## View Engine (`Giraffe.ViewEngine.Htmx`) ## View Engine (`Giraffe.ViewEngine.Htmx`)
As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the primary part of this library defines attributes that can be used within Giraffe views. Simply open `Giraffe.ViewEngine.Htmx`, and these attributes, along with support modules, will be visible. As htmx uses [attributes](https://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.
@@ -48,8 +53,10 @@ As htmx uses [attributes](https://htmx.org/docs/#attributes) to extend HTML, the
As an example, creating a `div` that loads data once the HTML is rendered: As an example, creating a `div` that loads data once the HTML is rendered:
```fsharp ```fsharp
let autoload = let autoload =
div [ _hxGet "/lazy-load-data"; _hxTrigger "load" ] [ str "Loading..." ] div [ _hxGet "/lazy-load-data"; _hxTrigger HxTrigger.Load ] [
str "Loading..."
]
``` ```
_(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true. To disable it for an element, use `_hxNoBoost` instead.)_ _(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true. To disable it for an element, use `_hxNoBoost` instead.)_
@@ -57,20 +64,20 @@ _(As `hx-boost="true"` is the usual desire for boosting, `_hxBoost` implies true
Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these, there are modules with those values. For example, `HxTrigger.Load` could be used in the example above, to ensure that the known values are spelled correctly. `hx-trigger` can also take modifiers, such as an action that only responds to `Ctrl`+click. The `HxTrigger` module has a `Filter` submodule to assist with defining these actions. Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these, there are modules with those values. For example, `HxTrigger.Load` could be used in the example above, to ensure that the known values are spelled correctly. `hx-trigger` can also take modifiers, such as an action that only responds to `Ctrl`+click. The `HxTrigger` module has a `Filter` submodule to assist with defining these actions.
```fsharp ```fsharp
let shiftClick = let shiftClick =
p [ _hxGet = "/something"; _hxTrigger (HxTrigger.Filter.Shift HxTrigger.Click) ] [ p [ _hxGet = "/something"; _hxTrigger (HxTrigger.Filter.Shift HxTrigger.Click) ] [
str "hold down Shift and click me" str "hold down Shift and click me"
] ]
``` ```
If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees. If you want to 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 ## Feedback / Help
The author hangs out in the #htmx-general channel of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/). The author hangs out in the #dotnet-htmx channel (and most others) of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/).
## Thanks ## Thanks
|[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)|[<img src="https://bitbadger.solutions/htmx-black-transparent.svg" alt="htmx logo" width="200">](https://htmx.org)|[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main)" width="200">](https://jb.gg/OpenSource)| |[<img src="https://giraffe.wiki/giraffe.png" alt="Giraffe logo" width="200">](https://giraffe.wiki)| [<img src="https://bitbadger.solutions/upload/bit-badger/2024/01/htmx-black-transparent.svg" alt="htmx logo" width="200">](https://htmx.org) |[<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/jb_beam.png" alt="JetBrains Logo (Main)" width="200">](https://jb.gg/OpenSource)|
| :---: | :---: | :---: | | :---: |:------------------------------------------------------------------------------------------------------------------------------------:| :---: |
|for making ASP.NET Core functional|for making HTML cool again|for licensing their tools to this project| |for making ASP.NET Core functional| for making HTML cool again |for licensing their tools to this project|
+96 -13
View File
@@ -1,28 +1,111 @@
/// Common definitions shared between attribute values and response headers /// <summary>Common definitions shared between attribute values and response headers</summary>
[<AutoOpen>] [<AutoOpen>]
module Giraffe.Htmx.Common module Giraffe.Htmx.Common
/// Valid values for the `hx-swap` attribute / `HX-Reswap` header (may be combined with swap/settle/scroll/show config) /// <summary>The version of htmx embedded in the package</summary>
let HtmxVersion = "4.0.0-beta4"
/// <summary>The path for the provided htmx script</summary>
let internal htmxLocalScript = $"/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"
/// <summary>The path for the provided htmax script</summary>
let internal htmaxLocalScript = $"/_content/Giraffe.Htmx.Common/htmax.min.js?ver={HtmxVersion}"
/// <summary>Serialize a list of key/value pairs to JSON (very rudimentary)</summary>
/// <param name="pairs">The key/value pairs to be serialized to JSON</param>
/// <returns>A string with the key/value pairs serialized to JSON</returns>
let internal toJson (pairs: (string * string) list) =
pairs
|> List.map (fun pair -> sprintf "\"%s\": \"%s\"" (fst pair) ((snd pair).Replace ("\"", "\\\"")))
|> String.concat ", "
|> sprintf "{ %s }"
/// <summary>Convert a boolean to lowercase "true" or "false"</summary>
/// <param name="boolValue">The boolean value to convert</param>
/// <returns>"true" for <c>true</c>, "false" for <c>false</c></returns>
let internal toLowerBool (boolValue: bool) =
(string boolValue).ToLowerInvariant()
/// <summary>Valid values for the <c>hx-swap</c> attribute / <c>HX-Reswap</c> header</summary>
/// <remarks>May be combined with <c>swap</c> / <c>scroll</c> / <c>show</c> config)</remarks>
/// <seealso href="https://four.htmx.org/attributes/hx-swap/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxSwap = module HxSwap =
/// The default, replace the inner html of the target element /// <summary>The default, replace the inner HTML of the target element</summary>
[<Literal>]
let InnerHtml = "innerHTML" let InnerHtml = "innerHTML"
/// Replace the entire target element with the response /// <summary>Replace the entire target element with the response</summary>
[<Literal>]
let OuterHtml = "outerHTML" let OuterHtml = "outerHTML"
/// Insert the response before the target element /// <summary>Morph the inner HTML of the target to the new content</summary>
let BeforeBegin = "beforebegin" [<Literal>]
let InnerMorph = "innerMorph"
/// Insert the response before the first child of the target element /// <summary>Morph the outer HTML of the target to the new content</summary>
let AfterBegin = "afterbegin" [<Literal>]
let OuterMorph = "outerMorph"
/// Insert the response after the last child of the target element /// <summary>Morph the outer HTML of the target to the new content, recreating all children</summary>
let BeforeEnd = "beforeend" /// <remarks>This is used internally by the new history extension, but can be used by others if desired</remarks>
[<Literal>]
let OuterSync = "outerSync"
/// Insert the response after the target element /// <summary>Replace the text content of the target without parsing the response as HTML</summary>
let AfterEnd = "afterend" [<Literal>]
let TextContent = "textContent"
/// Does not append content from response (out of band items will still be processed). /// <summary>Insert the response before the target element</summary>
[<Literal>]
let Before = "before"
/// <summary>Insert the response before the target element (pre-v4 name)</summary>
[<Literal>]
let BeforeBegin = Before
/// <summary>Insert the response before the first child of the target element</summary>
[<Literal>]
let Prepend = "prepend"
/// <summary>Insert the response before the first child of the target element (pre-v4 name)</summary>
[<Literal>]
let AfterBegin = Prepend
/// <summary>Insert the response after the last child of the target element</summary>
[<Literal>]
let Append = "append"
/// <summary>Insert the response after the last child of the target element (pre-v4 name)</summary>
[<Literal>]
let BeforeEnd = Append
/// <summary>Insert the response after the target element</summary>
[<Literal>]
let After = "after"
/// <summary>Insert the response after the target element (pre-v4 name)</summary>
[<Literal>]
let AfterEnd = After
/// <summary>Delete the target element regardless of response</summary>
[<Literal>]
let Delete = "delete"
/// <summary>Does not append content from response (out of band items will still be processed)</summary>
[<Literal>]
let None = "none" 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"
+11 -1
View File
@@ -1,14 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup> <PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Common definitions for Giraffe.Htmx</Description> <Description>Common definitions for Giraffe.Htmx</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Common.fs" /> <Compile Include="Common.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="FSharp.Core" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Giraffe.Htmx" />
<InternalsVisibleTo Include="Giraffe.ViewEngine.Htmx" />
</ItemGroup>
</Project> </Project>
+4 -2
View File
@@ -1,5 +1,7 @@
## Giraffe.Htmx.Common ## Giraffe.Htmx.Common
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one. 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: 1.9.8** **htmx version: 4.0.0-beta4**
_**NOTE:** Pay special attention to breaking changes highlighted in the packages listed above._
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+16 -5
View File
@@ -1,14 +1,25 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?> <?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup> <PropertyGroup>
<TargetFrameworks>net6.0;net7.0;net8.0</TargetFrameworks> <TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<VersionPrefix>1.9.8</VersionPrefix> <VersionPrefix>4.0.0</VersionPrefix>
<PackageReleaseNotes>Update script tags to pull htmx 1.9.8; add support for .NET 8</PackageReleaseNotes> <VersionSuffix>beta4</VersionSuffix>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageReleaseNotes>Update htmx 4 to beta4
- [Common] Update provided htmx 4 to 4.0.0-beta4
- [Common] Add htmax bundle to provided version
- [Server] Update HX-Target header to return tag and ID (similar to HX-Source)
- [Server] Add support for HX-Request-Type header
- [View Engine] Add hx-status attribute, outerSync swap
- [View Engine] Updated script tags to pull htmx 4.0.0-beta4, added "max" bundle links for local and CDN
See package and prior alpha release READMEs; v2 to v4 is not an update-and-forget-it release
</PackageReleaseNotes>
<Authors>danieljsummers</Authors> <Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company> <Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl> <PackageProjectUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</PackageProjectUrl>
<PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance> <PackageRequireLicenseAcceptance>false</PackageRequireLicenseAcceptance>
<RepositoryUrl>https://github.com/bit-badger/Giraffe.Htmx</RepositoryUrl> <RepositoryUrl>https://git.bitbadger.solutions/bit-badger/Giraffe.Htmx</RepositoryUrl>
<RepositoryType>Git</RepositoryType> <RepositoryType>Git</RepositoryType>
<Copyright>MIT License</Copyright> <Copyright>MIT License</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression> <PackageLicenseExpression>MIT</PackageLicenseExpression>
+13
View File
@@ -0,0 +1,13 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Expecto" Version="11.0.0" />
<PackageVersion Include="Giraffe" Version="8.2.0" />
<PackageVersion Include="Giraffe.ViewEngine" Version="1.4.0" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="FSharp.Core" Version="10.1.301" />
<!-- <PackageVersion Update="FSharp.Core" Version="10.0.0" /> -->
</ItemGroup>
</Project>
-40
View File
@@ -1,40 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx", "Htmx\Giraffe.Htmx.fsproj", "{8AB3085C-5236-485A-8565-A09106E72E1E}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.ViewEngine.Htmx", "ViewEngine.Htmx\Giraffe.ViewEngine.Htmx.fsproj", "{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Giraffe.Htmx.Common", "Common\Giraffe.Htmx.Common.fsproj", "{75D66845-F93A-4463-AD29-A8B16E4D4BA9}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Tests", "Tests\Tests.fsproj", "{39823773-4311-4E79-9CA0-F9DDC40CAF6A}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8AB3085C-5236-485A-8565-A09106E72E1E}.Release|Any CPU.Build.0 = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F718B3C1-EE01-4F04-ABCE-BF2AE700FDA9}.Release|Any CPU.Build.0 = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{75D66845-F93A-4463-AD29-A8B16E4D4BA9}.Release|Any CPU.Build.0 = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39823773-4311-4E79-9CA0-F9DDC40CAF6A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal
+6
View File
@@ -0,0 +1,6 @@
<Solution>
<Project Path="Common/Giraffe.Htmx.Common.fsproj" />
<Project Path="Htmx/Giraffe.Htmx.fsproj" />
<Project Path="Tests/Tests.fsproj" />
<Project Path="ViewEngine.Htmx/Giraffe.ViewEngine.Htmx.fsproj" />
</Solution>
+4 -1
View File
@@ -4,15 +4,18 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>htmx header extensions and helpers for Giraffe</Description> <Description>htmx header extensions and helpers for Giraffe</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe" Version="5.0.0" /> <PackageReference Include="Giraffe" />
<PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+199 -76
View File
@@ -4,6 +4,14 @@ open Microsoft.AspNetCore.Http
open Microsoft.Extensions.Primitives open Microsoft.Extensions.Primitives
open System open System
/// <summary>The request types which may be set in the <c>HX-Request</c> header</summary>
type HxRequestTypes =
/// <summary>A request targeting the <c>body</c> tag or using an <c>hx-select</c> attribute</summary>
| HxFullRequest
/// <summary>A request for partial content</summary>
| HxPartialRequest
/// Determine if the given header is present /// Determine if the given header is present
let private hdr (headers : IHeaderDictionary) hdr = let private hdr (headers : IHeaderDictionary) hdr =
match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0] match headers[hdr] with it when it = StringValues.Empty -> None | it -> Some it[0]
@@ -11,121 +19,236 @@ let private hdr (headers : IHeaderDictionary) hdr =
/// Extensions to the header dictionary /// Extensions to the header dictionary
type IHeaderDictionary with type IHeaderDictionary with
/// Indicates that the request is via an element using `hx-boost` /// <summary>Indicates that the request is via an element using <c>hx-boost</c></summary>
member this.HxBoosted with get () = hdr this "HX-Boosted" |> Option.map bool.Parse member this.HxBoosted
with get () = hdr this "HX-Boosted" |> Option.map bool.Parse
/// The current URL of the browser _(note that this does not update until after settle)_ /// <summary>The current URL of the browser <em>(note that this does not update until after settle)</em></summary>
member this.HxCurrentUrl with get () = hdr this "HX-Current-URL" |> Option.map Uri member this.HxCurrentUrl
with get () = hdr this "HX-Current-URL" |> Option.map Uri
/// `true` if the request is for history restoration after a miss in the local history cache /// <summary><c>true</c> if the request is for history restoration after a miss in the local history cache</summary>
member this.HxHistoryRestoreRequest with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse member this.HxHistoryRestoreRequest
with get () = hdr this "HX-History-Restore-Request" |> Option.map bool.Parse
/// The user response to an `hx-prompt` /// <summary><c>true</c> if the request has been fired by the preload extension</summary>
member this.HxPrompt with get () = hdr this "HX-Prompt" /// <remarks><c>preload</c> is part of the htmax htmx-plus-extensions bundle</remarks>
member this.HxPreloaded
with get () = hdr this "HX-Preloaded" |> Option.map bool.Parse
/// `true` if the request came from HTMX /// <summary>The user response to an <c>hx-prompt</c></summary>
member this.HxRequest with get () = hdr this "HX-Request" |> Option.map bool.Parse [<Obsolete "hx-prompt is removed in v4">]
member this.HxPrompt
with get () = hdr this "HX-Prompt"
/// The `id` of the target element if it exists /// <summary><c>true</c> if the request came from htmx</summary>
member this.HxTarget with get () = hdr this "HX-Target" member this.HxRequest
with get () = hdr this "HX-Request" |> Option.map bool.Parse
/// The `id` of the triggered element if it exists /// <summary>The ID of the request (WebSocket extension requests only)</summary>
member this.HxTrigger with get () = hdr this "HX-Trigger" /// <remarks><c>hx-ws</c> is part of the htmax htmx-plus-extensions bundle</remarks>
member this.HxRequestId
with get () = hdr this "HX-Request-ID"
/// The `name` of the triggered element if it exists /// <summary>The request type sent by htmx</summary>
member this.HxTriggerName with get () = hdr this "HX-Trigger-Name" /// <seealso cref="HxRequestTypes" />
member this.HxRequestType
with get () =
match hdr this "HX-Request-Type" with
| Some typ when typ = "full" -> Some HxFullRequest
| Some typ when typ = "partial" -> Some HxPartialRequest
| Some _ -> None
| None -> None
/// <summary>The tag name (fst) and <c>id</c> attribute (snd) of the element triggering this request</summary>
member this.HxSource
with get () =
match hdr this "HX-Source" with
| Some src ->
let parts = src.Split "#"
if parts.Length = 1 then
Some (parts[0], None)
else
Some (parts[0], if parts[1] <> "" then Some parts[1] else None)
| None -> None
/// <summary>The <c>id</c> attribute of the target element if it exists</summary>
/// <remarks>
/// In v4, this changed to tag name (fst) and <c>id</c> (snd); to resolve build errors, and restore the prior
/// behavior of <c>[id] option</c>, replace
/// <code>ctx.HxTarget</code>
/// with
/// <code>ctx.HxTarget |> Option.iter snd</code>
/// </remarks>
member this.HxTarget
with get () =
match hdr this "HX-Target" with
| Some src ->
let parts = src.Split "#"
if parts.Length = 1 then
Some (parts[0], None)
else
Some (parts[0], if parts[1] <> "" then Some parts[1] else None)
| None -> None
/// <summary>The <c>id</c> attribute of the triggered element if it exists</summary>
[<Obsolete "HX-Trigger is removed in v4; use the second item of HX-Source">]
member this.HxTrigger
with get () = hdr this "HX-Trigger"
/// <summary>The <c>name</c> attribute of the triggered element if it exists</summary>
[<Obsolete "HX-Trigger-Name is removed in v4; may be available via extension, but will be removed from this library">]
member this.HxTriggerName
with get () = hdr this "HX-Trigger-Name"
/// Extensions for the request object /// Extensions for the request object
type HttpRequest with type HttpRequest with
/// Whether this request was initiated from htmx /// <summary>Whether this request was initiated from htmx</summary>
member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false member this.IsHtmx
with get () = this.Headers.HxRequest |> Option.defaultValue false
/// Whether this request is an htmx history-miss refresh request /// <summary>Whether this request is an htmx history-miss refresh request</summary>
member this.IsHtmxRefresh with get () = member this.IsHtmxRefresh
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false) with get () = this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
/// HTTP handlers for setting output headers /// <summary>HTTP handlers for setting output headers</summary>
[<AutoOpen>] [<AutoOpen>]
module Handlers = module Handlers =
/// Convert a boolean to lowercase `true` or `false` open Giraffe.Htmx.Common
let private toLowerBool (trueOrFalse : bool) =
(string trueOrFalse).ToLowerInvariant ()
/// Serialize a list of key/value pairs to JSON (very rudimentary) /// <summary>Instruct htmx to download a response from another path / URL</summary>
let private toJson (evts : (string * string) list) = /// <param name="path">The path or URL where the downloadable content is found</param>
evts /// <returns>An HTTP handler with the <c>HX-Download</c> header set</returns>
|> List.map (fun evt -> sprintf "\"%s\": \"%s\"" (fst evt) ((snd evt).Replace ("\"", "\\\""))) /// <remarks>This requires the client-side <c>hx-download</c> extension (included in the htmax bundle)</remarks>
|> String.concat ", " /// <seealso href="https://four.htmx.org/extensions/hx-download#hx-download-header">Documentation</seealso>
|> sprintf "{ %s }" let withHxDownload (path: string) : HttpHandler =
setHttpHeader "HX-Download" path
/// Pushes a new url into the history stack /// <summary>Instruct htmx to perform a client-side redirect for content</summary>
let withHxPushUrl : string -> HttpHandler = /// <param name="path">The path where the content should be found</param>
setHttpHeader "HX-Push-Url" /// <returns>An HTTP handler with the <c>HX-Location</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-location/">Documentation</seealso>
let withHxLocation (path: string) : HttpHandler =
setHttpHeader "HX-Location" path
/// Explicitly do not push a new URL into the history stack /// <summary>Pushes a new url into the history stack</summary>
/// <param name="url">The URL to be pushed</param>
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set</returns>
/// <remarks>Use <see cref="withHxNoPushUrl" /> to explicitly not push a new URL</remarks>
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
let withHxPushUrl (url: string) : HttpHandler =
setHttpHeader "HX-Push-Url" url
/// <summary>Explicitly do not push a new URL into the history stack</summary>
/// <returns>An HTTP handler with the <c>HX-Push-Url</c> header set to <c>false</c></returns>
/// <seealso href="https://htmx.org/headers/hx-push-url/">Documentation</seealso>
let withHxNoPushUrl : HttpHandler = let withHxNoPushUrl : HttpHandler =
toLowerBool false |> withHxPushUrl toLowerBool false |> withHxPushUrl
/// Pushes a new url into the history stack /// <summary>Can be used to do a client-side redirect to a new location</summary>
[<Obsolete "Use withHxPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">] /// <param name="url">The URL to which the client should be redirected</param>
let withHxPush = withHxPushUrl /// <returns>An HTTP handler with the <c>HX-Redirect</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-redirect/">Documentation</seealso>
let withHxRedirect (url: string) : HttpHandler =
setHttpHeader "HX-Redirect" url
/// Explicitly do not push a new URL into the history stack /// <summary>If set to <c>true</c> the client side will do a full refresh of the page</summary>
[<Obsolete "Use withHxNoPushUrl; HX-Push was replaced by HX-Push-Url in v1.8.0">] /// <param name="shouldRefresh">Whether the client should refresh their page</param>
let withHxNoPush = withHxNoPushUrl /// <returns>An HTTP handler with the <c>HX-Refresh</c> header set</returns>
let withHxRefresh shouldRefresh : HttpHandler =
(toLowerBool >> setHttpHeader "HX-Refresh") shouldRefresh
/// Can be used to do a client-side redirect to a new location /// <summary>Replaces the current URL in the history stack</summary>
let withHxRedirect : string -> HttpHandler = /// <param name="url">The URL to place in the history stack in place of the current one</param>
setHttpHeader "HX-Redirect" /// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set</returns>
/// <remarks>Use <see cref="withHxNoRelaceUrl" /> to explicitly not replace the current URL</remarks>
/// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
let withHxReplaceUrl url : HttpHandler =
setHttpHeader "HX-Replace-Url" url
/// If set to `true` the client side will do a a full refresh of the page /// <summary>Explicitly do not replace the current URL in the history stack</summary>
let withHxRefresh : bool -> HttpHandler = /// <returns>An HTTP handler with the <c>HX-Replace-URL</c> header set to <c>false</c></returns>
toLowerBool >> setHttpHeader "HX-Refresh" /// <seealso href="https://htmx.org/headers/hx-replace-url/">Documentation</seealso>
/// Replaces the current URL in the history stack
let withHxReplaceUrl : string -> HttpHandler =
setHttpHeader "HX-Replace-Url"
/// Explicitly do not replace the current URL in the history stack
let withHxNoReplaceUrl : HttpHandler = let withHxNoReplaceUrl : HttpHandler =
toLowerBool false |> withHxReplaceUrl toLowerBool false |> withHxReplaceUrl
/// Override which portion of the response will be swapped into the target document /// <summary>Override which portion of the response will be swapped into the target document</summary>
let withHxReselect : string -> HttpHandler = /// <param name="target">The selector for the new response target</param>
setHttpHeader "HX-Reselect" /// <returns>An HTTP handler with the <c>HX-Reselect</c> header set</returns>
let withHxReselect (target: string) : HttpHandler =
setHttpHeader "HX-Reselect" target
/// Override the `hx-swap` attribute from the initiating element /// <summary>Override the <c>hx-swap</c> attribute from the initiating element</summary>
let withHxReswap : string -> HttpHandler = /// <param name="swap">The swap value to override</param>
setHttpHeader "HX-Reswap" /// <returns>An HTTP handler with the <c>HX-Reswap</c> header set</returns>
/// <remarks>Use <see cref="T:Giraffe.Htmx.Common.HxSwap">HxSwap</see> constants for best results</remarks>
let withHxReswap (swap: string) : HttpHandler =
setHttpHeader "HX-Reswap" swap
/// Allows you to override the `hx-target` attribute /// <summary>Allows you to override the <c>hx-target</c> attribute</summary>
let withHxRetarget : string -> HttpHandler = /// <param name="target">The new target for the response</param>
setHttpHeader "HX-Retarget" /// <returns>An HTTP handler with the <c>HX-Retarget</c> header set</returns>
let withHxRetarget (target: string) : HttpHandler =
setHttpHeader "HX-Retarget" target
/// Allows you to trigger a single client side event /// <summary>Allows you to trigger a single client side event</summary>
let withHxTrigger : string -> HttpHandler = /// <param name="evt">The call to the event that should be triggered</param>
setHttpHeader "HX-Trigger" /// <returns>An HTTP handler with the <c>HX-Trigger</c> header set</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTrigger (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger" evt
/// Allows you to trigger multiple client side events /// <summary>Allows you to trigger multiple client side events</summary>
/// <param name="evts">The calls to events that should be triggered</param>
/// <returns>An HTTP handler with the <c>HX-Trigger</c> header set for all given events</returns>
/// <seealso href="https://htmx.org/headers/hx-trigger/">Documentation</seealso>
let withHxTriggerMany evts : HttpHandler = let withHxTriggerMany evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger" toJson evts |> setHttpHeader "HX-Trigger"
/// Allows you to trigger a single client side event after changes have settled /// <summary>Allows you to trigger a single client side event after changes have settled</summary>
let withHxTriggerAfterSettle : string -> HttpHandler = /// <param name="evt">The call to the event that should be triggered</param>
setHttpHeader "HX-Trigger-After-Settle" /// <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>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerAfterSettle (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger" evt
/// Allows you to trigger multiple client side events after changes have settled /// <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>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerManyAfterSettle evts : HttpHandler = let withHxTriggerManyAfterSettle evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Settle" toJson evts |> setHttpHeader "HX-Trigger"
/// Allows you to trigger a single client side event after DOM swapping occurs /// <summary>Allows you to trigger a single client side event after DOM swapping occurs</summary>
let withHxTriggerAfterSwap : string -> HttpHandler = /// <param name="evt">The call to the event that should be triggered</param>
setHttpHeader "HX-Trigger-After-Swap" /// <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>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerAfterSwap (evt: string) : HttpHandler =
setHttpHeader "HX-Trigger" evt
/// Allows you to trigger multiple client side events after DOM swapping occurs /// <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>
[<Obsolete "Removed in v4; use withHxTrigger">]
let withHxTriggerManyAfterSwap evts : HttpHandler = let withHxTriggerManyAfterSwap evts : HttpHandler =
toJson evts |> setHttpHeader "HX-Trigger-After-Swap" toJson evts |> setHttpHeader "HX-Trigger"
/// <summary>Load the package-provided version of the htmx script</summary>
[<RequireQualifiedAccess>]
module HtmxScript =
open Giraffe.Htmx.Common
open Microsoft.AspNetCore.Html
/// <summary><c>script</c> tag to load the package-provided version of the htmx script</summary>
let local = HtmlString $"""<script src="{htmxLocalScript}"></script>"""
+12 -6
View File
@@ -2,7 +2,11 @@
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`. This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 1.9.8** **htmx version: 4.0.0-beta4**
_Upgrading from v2.x: the [migration guide](https://four.htmx.org/docs/get-started/migration) lists changes for v4. For this package, the `HX-Trigger` and `HX-Trigger-Name` headers are marked obsolete. They are replaced by `HX-Source`, which provides the triggering tag name and `id` attribute. The `HX-Prompt` header has also been marked as obsolete, as the `hx-prompt` attribute which generated its content has been removed._
_Obsolete elements will be removed in the first production v4 release._
### Setup ### Setup
@@ -14,17 +18,17 @@ This package enables server-side support for [htmx](https://htmx.org) within [Gi
To obtain a request header, using the `IHeaderDictionary` extension properties: To obtain a request header, using the `IHeaderDictionary` extension properties:
```fsharp ```fsharp
let myHandler : HttpHander = let myHandler : HttpHander =
fun next ctx -> fun next ctx ->
match ctx.HxPrompt with match ctx.Target with
| Some prompt -> ... // do something with the text the user provided | Some elt -> ... // do something with id of the target element
| None -> ... // no text provided | None -> ... // no target element provided
``` ```
To set a response header: To set a response header:
```fsharp ```fsharp
let myHandler : HttpHander = let myHandler : HttpHander =
fun next ctx -> fun next ctx ->
// some meaningful work // some meaningful work
withHxPushUrl "/some/new/url" >=> [other handlers] withHxPushUrl "/some/new/url" >=> [other handlers]
@@ -32,6 +36,8 @@ To set a response header:
The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information. The `HxSwap` module has constants to use for the `HX-Reswap` header. These may be extended with settle, show, and other qualifiers; see the htmx documentation for the `hx-swap` attribute for more information.
To load the package-provided htmx library without using Giraffe.ViewEngine, use `HtmxScript.local`.
### Learn ### Learn
The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events. The naming conventions of this library were selected to mirror those provided by htmx. The header properties become `Hx*` on the `ctx.Request.Headers` object, and the response handlers are `withHx*` based on the header being set. The only part that does not line up is `withHxTrigger*` and `withHxTriggerMany`; the former set work with a single string (to trigger a single event with no arguments), while the latter set supports both arguments and multiple events.
+46 -7
View File
@@ -3,6 +3,12 @@ module Common
open Expecto open Expecto
open Giraffe.Htmx open Giraffe.Htmx
/// Test to ensure the version was updated
let version =
test "HtmxVersion is correct" {
Expect.equal HtmxVersion "4.0.0-beta4" "htmx version incorrect"
}
/// Tests for the HxSwap module /// Tests for the HxSwap module
let swap = let swap =
testList "HxSwap" [ testList "HxSwap" [
@@ -12,22 +18,55 @@ let swap =
test "OuterHtml is correct" { test "OuterHtml is correct" {
Expect.equal HxSwap.OuterHtml "outerHTML" "Outer HTML swap value incorrect" Expect.equal HxSwap.OuterHtml "outerHTML" "Outer HTML swap value incorrect"
} }
test "BeforeBegin is correct" { test "InnerMorph is correct" {
Expect.equal HxSwap.BeforeBegin "beforebegin" "Before Begin swap value incorrect" Expect.equal HxSwap.InnerMorph "innerMorph" "Inner Morph swap value incorrect"
} }
test "BeforeEnd is correct" { test "OuterMorph is correct" {
Expect.equal HxSwap.BeforeEnd "beforeend" "Before End swap value incorrect" Expect.equal HxSwap.OuterMorph "outerMorph" "Outer Morph swap value incorrect"
}
test "OuterSync is correct" {
Expect.equal HxSwap.OuterSync "outerSync" "Outer Sync swap value incorrect"
}
test "TextContent is correct" {
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" { test "AfterBegin is correct" {
Expect.equal HxSwap.AfterBegin "afterbegin" "After Begin swap value incorrect" Expect.equal HxSwap.AfterBegin HxSwap.Prepend "Prepend swap value incorrect"
}
test "Append is correct" {
Expect.equal HxSwap.Append "append" "Append swap value incorrect"
}
test "BeforeEnd is correct" {
Expect.equal HxSwap.BeforeEnd HxSwap.Append "Before End swap value incorrect"
}
test "After is correct" {
Expect.equal HxSwap.After "after" "After swap value incorrect"
} }
test "AfterEnd is correct" { test "AfterEnd is correct" {
Expect.equal HxSwap.AfterEnd "afterend" "After End swap value incorrect" Expect.equal HxSwap.AfterEnd HxSwap.After "After End swap value incorrect"
}
test "Delete is correct" {
Expect.equal HxSwap.Delete "delete" "Delete swap value incorrect"
} }
test "None is correct" { test "None is correct" {
Expect.equal HxSwap.None "none" "None swap value incorrect" 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 /// All tests for this module
let allTests = testList "Htmx.Common" [ swap ] let allTests = testList "Htmx.Common" [ version; swap ]
+295 -135
View File
@@ -11,22 +11,22 @@ let dictExtensions =
testList "IHeaderDictionaryExtensions" [ testList "IHeaderDictionaryExtensions" [
testList "HxBoosted" [ testList "HxBoosted" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxBoosted "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Boosted", "true") dic.Add("HX-Boosted", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true" Expect.isTrue ctx.Request.Headers.HxBoosted.Value "The header value should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Boosted", "false") dic.Add("HX-Boosted", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present" Expect.isSome ctx.Request.Headers.HxBoosted "There should be a header present"
Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false" Expect.isFalse ctx.Request.Headers.HxBoosted.Value "The header value should have been false"
@@ -34,14 +34,14 @@ let dictExtensions =
] ]
testList "HxCurrentUrl" [ testList "HxCurrentUrl" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxCurrentUrl "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Current-URL", "http://localhost/test.htm") dic.Add("HX-Current-URL", "http://localhost/test.htm")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present" Expect.isSome ctx.Request.Headers.HxCurrentUrl "There should be a header present"
Expect.equal Expect.equal
@@ -51,109 +51,194 @@ let dictExtensions =
] ]
testList "HxHistoryRestoreRequest" [ testList "HxHistoryRestoreRequest" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxHistoryRestoreRequest "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-History-Restore-Request", "true") dic.Add("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true" Expect.isTrue ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header value should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-History-Restore-Request", "false") dic.Add("HX-History-Restore-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present" Expect.isSome ctx.Request.Headers.HxHistoryRestoreRequest "There should be a header present"
Expect.isFalse Expect.isFalse
ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header should have been false" ctx.Request.Headers.HxHistoryRestoreRequest.Value "The header should have been false"
} }
] ]
testList "HxPrompt" [ testList "HxPreloaded" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxPreloaded "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Prompt", "of course") dic.Add("HX-Preloaded", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present" Expect.isSome ctx.Request.Headers.HxPreloaded "There should be a header present"
Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect" 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 "HxRequest" [ testList "HxRequest" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxRequest "There should not have been a header returned"
} }
test "succeeds when the header is present and true" { test "succeeds when the header is present and true" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Request", "true") dic.Add("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true" Expect.isTrue ctx.Request.Headers.HxRequest.Value "The header should have been true"
} }
test "succeeds when the header is present and false" { test "succeeds when the header is present and false" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Request", "false") dic.Add("HX-Request", "false")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present" Expect.isSome ctx.Request.Headers.HxRequest "There should be a header present"
Expect.isFalse ctx.Request.Headers.HxRequest.Value "The header should have been false" 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" [ testList "HxTarget" [
test "succeeds when the header is not present" { test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned" Expect.isNone ctx.Request.Headers.HxTarget "There should not have been a header returned"
} }
test "succeeds when the header is present" { test "succeeds when the header is present and both parts exist" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Target", "#leItem") dic.Add("HX-Target", "div#leItem")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTarget "There should be a header present" let hdr = ctx.Request.Headers.HxTarget
Expect.equal ctx.Request.Headers.HxTarget.Value "#leItem" "The header value was incorrect" Expect.isSome hdr "There should be a header present"
Expect.equal (fst hdr.Value) "div" "The target tag was incorrect"
Expect.isSome (snd hdr.Value) "There should be a target ID present"
Expect.equal (snd hdr.Value).Value "leItem" "The header value was incorrect"
} }
] test "succeeds when the header is present and ID is blank" {
testList "HxTrigger" [ let ctx = Substitute.For<HttpContext>()
test "succeeds when the header is not present" { let dic = HeaderDictionary()
let ctx = Substitute.For<HttpContext> () dic.Add("HX-Target", "span#")
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
dic.Add ("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present" let hdr = ctx.Request.Headers.HxTarget
Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect" Expect.isSome hdr "There should be a header present"
Expect.equal (fst hdr.Value) "span" "The target tag was incorrect"
Expect.isNone (snd hdr.Value) "There should not be a target ID present"
} }
] test "succeeds when the header is present and ID is missing" {
testList "HxTriggerName" [ let ctx = Substitute.For<HttpContext>()
test "succeeds when the header is not present" { let dic = HeaderDictionary()
let ctx = Substitute.For<HttpContext> () dic.Add("HX-Target", "aside")
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 ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present" let hdr = ctx.Request.Headers.HxTarget
Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect" 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"
} }
] ]
] ]
@@ -163,36 +248,36 @@ let reqExtensions =
testList "HttpRequestExtensions" [ testList "HttpRequestExtensions" [
testList "IsHtmx" [ testList "IsHtmx" [
test "succeeds when request is not from htmx" { test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request" Expect.isFalse ctx.Request.IsHtmx "The request should not be an htmx request"
} }
test "succeeds when request is from htmx" { test "succeeds when request is from htmx" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Request", "true") dic.Add("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request" Expect.isTrue ctx.Request.IsHtmx "The request should have been an htmx request"
} }
] ]
testList "IsHtmxRefresh" [ testList "IsHtmxRefresh" [
test "succeeds when request is not from htmx" { test "succeeds when request is not from htmx" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
} }
test "succeeds when request is from htmx, but not a refresh" { test "succeeds when request is from htmx, but not a refresh" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Request", "true") dic.Add("HX-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh" Expect.isFalse ctx.Request.IsHtmxRefresh "The request should not have been an htmx refresh"
} }
test "IsHtmxRefresh succeeds when request is from htmx and is a refresh" { test "IsHtmxRefresh succeeds when request is from htmx and is a refresh" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
dic.Add ("HX-Request", "true") dic.Add("HX-Request", "true")
dic.Add ("HX-History-Restore-Request", "true") dic.Add("HX-History-Restore-Request", "true")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh" Expect.isTrue ctx.Request.IsHtmxRefresh "The request should have been an htmx refresh"
} }
@@ -202,30 +287,46 @@ let reqExtensions =
open System.Threading.Tasks open System.Threading.Tasks
/// Dummy "next" parameter to get the pipeline to execute/terminate /// Dummy "next" parameter to get the pipeline to execute/terminate
let next (ctx : HttpContext) = Task.FromResult (Some ctx) let next (ctx: HttpContext) = Task.FromResult(Some ctx)
/// Tests for the HttpHandler functions provided in the Handlers module /// Tests for the HttpHandler functions provided in the Handlers module
let handlers = let handlers =
testList "HandlerTests" [ testList "HandlerTests" [
testTask "withHxDownload succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxDownload "/files/stuff.pdf" next ctx
Expect.isTrue (dic.ContainsKey "HX-Download") "The HX-Download header should be present"
Expect.equal dic["HX-Download"].[0] "/files/stuff.pdf" "The HX-Download value was incorrect"
}
testTask "withHxLocation succeeds" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxLocation "/pagina-otro.html" next ctx
Expect.isTrue (dic.ContainsKey "HX-Location") "The HX-Location header should be present"
Expect.equal dic["HX-Location"].[0] "/pagina-otro.html" "The HX-Location value was incorrect"
}
testTask "withHxPushUrl succeeds" { testTask "withHxPushUrl succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxPushUrl "/a-new-url" next ctx let! _ = withHxPushUrl "/a-new-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect" Expect.equal dic["HX-Push-Url"].[0] "/a-new-url" "The HX-Push-Url value was incorrect"
} }
testTask "withHxNoPushUrl succeeds" { testTask "withHxNoPushUrl succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoPushUrl next ctx let! _ = withHxNoPushUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present" Expect.isTrue (dic.ContainsKey "HX-Push-Url") "The HX-Push-Url header should be present"
Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect" Expect.equal dic["HX-Push-Url"].[0] "false" "The HX-Push-Url value was incorrect"
} }
testTask "withHxRedirect succeeds" { testTask "withHxRedirect succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRedirect "/somewhere-else" next ctx let! _ = withHxRedirect "/somewhere-else" next ctx
Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present" Expect.isTrue (dic.ContainsKey "HX-Redirect") "The HX-Redirect header should be present"
@@ -233,16 +334,16 @@ let handlers =
} }
testList "withHxRefresh" [ testList "withHxRefresh" [
testTask "succeeds when set to true" { testTask "succeeds when set to true" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh true next ctx let! _ = withHxRefresh true next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect" Expect.equal dic["HX-Refresh"].[0] "true" "The HX-Refresh value was incorrect"
} }
testTask "succeeds when set to false" { testTask "succeeds when set to false" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRefresh false next ctx let! _ = withHxRefresh false next ctx
Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present" Expect.isTrue (dic.ContainsKey "HX-Refresh") "The HX-Refresh header should be present"
@@ -250,101 +351,160 @@ let handlers =
} }
] ]
testTask "withHxReplaceUrl succeeds" { testTask "withHxReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReplaceUrl "/a-substitute-url" next ctx let! _ = withHxReplaceUrl "/a-substitute-url" next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect" Expect.equal dic["HX-Replace-Url"].[0] "/a-substitute-url" "The HX-Replace-Url value was incorrect"
} }
testTask "withHxNoReplaceUrl succeeds" { testTask "withHxNoReplaceUrl succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxNoReplaceUrl next ctx let! _ = withHxNoReplaceUrl next ctx
Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present" Expect.isTrue (dic.ContainsKey "HX-Replace-Url") "The HX-Replace-Url header should be present"
Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect" Expect.equal dic["HX-Replace-Url"].[0] "false" "The HX-Replace-Url value was incorrect"
} }
testTask "withHxReselect succeeds" { testTask "withHxReselect succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReselect "#test" next ctx let! _ = withHxReselect "#test" next ctx
Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present" Expect.isTrue (dic.ContainsKey "HX-Reselect") "The HX-Reselect header should be present"
Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect" Expect.equal dic["HX-Reselect"].[0] "#test" "The HX-Reselect value was incorrect"
} }
testTask "withHxReswap succeeds" { testTask "withHxReswap succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxReswap HxSwap.BeforeEnd next ctx let! _ = withHxReswap HxSwap.BeforeEnd next ctx
Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present" Expect.isTrue (dic.ContainsKey "HX-Reswap") "The HX-Reswap header should be present"
Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect" Expect.equal dic["HX-Reswap"].[0] HxSwap.BeforeEnd "The HX-Reswap value was incorrect"
} }
testTask "withHxRetarget succeeds" { testTask "withHxRetarget succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxRetarget "#somewhereElse" next ctx let! _ = withHxRetarget "#somewhereElse" next ctx
Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present" Expect.isTrue (dic.ContainsKey "HX-Retarget") "The HX-Retarget header should be present"
Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect" Expect.equal dic["HX-Retarget"].[0] "#somewhereElse" "The HX-Retarget value was incorrect"
} }
testTask "withHxTrigger succeeds" { testTask "withHxTrigger succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTrigger "doSomething" next ctx let! _ = withHxTrigger "doSomething" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect" Expect.equal dic["HX-Trigger"].[0] "doSomething" "The HX-Trigger value was incorrect"
} }
testTask "withHxTriggerMany succeeds" { testTask "withHxTriggerMany succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx let! _ = withHxTriggerMany [ "blah", "foo"; "bleh", "bar" ] next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present" Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal Expect.equal
dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect" dic["HX-Trigger"].[0] """{ "blah": "foo", "bleh": "bar" }""" "The HX-Trigger value was incorrect"
} }
]
/// Tests for the HtmxScript module
let script =
testList "HtmxScript" [
test "local generates correct link" {
Expect.equal
(string HtmxScript.local)
$"""<script src="/_content/Giraffe.Htmx.Common/htmx.min.js?ver={HtmxVersion}"></script>"""
"htmx script link is incorrect"
}
]
#nowarn 44 // Obsolete items still have tests
let dictExtensionsObs =
testList "IHeaderDictionaryExtensions (Obsolete)" [
testList "HxPrompt" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxPrompt "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Prompt", "of course")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxPrompt "There should be a header present"
Expect.equal ctx.Request.Headers.HxPrompt.Value "of course" "The header value was incorrect"
}
]
testList "HxTrigger" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs (HeaderDictionary ()) |> ignore
Expect.isNone ctx.Request.Headers.HxTrigger "There should not have been a header returned"
}
test "succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Trigger", "#trig")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTrigger "There should be a header present"
Expect.equal ctx.Request.Headers.HxTrigger.Value "#trig" "The header value was incorrect"
}
]
testList "HxTriggerName" [
test "succeeds when the header is not present" {
let ctx = Substitute.For<HttpContext>()
ctx.Request.Headers.ReturnsForAnyArgs(HeaderDictionary()) |> ignore
Expect.isNone ctx.Request.Headers.HxTriggerName "There should not have been a header returned"
}
test "HxTriggerName succeeds when the header is present" {
let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary()
dic.Add("HX-Trigger-Name", "click")
ctx.Request.Headers.ReturnsForAnyArgs dic |> ignore
Expect.isSome ctx.Request.Headers.HxTriggerName "There should be a header present"
Expect.equal ctx.Request.Headers.HxTriggerName.Value "click" "The header value was incorrect"
}
]
]
let handlerObs =
testList "Handler Tests (Obsolete)" [
testTask "withHxTriggerAfterSettle succeeds" { testTask "withHxTriggerAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSettle "byTheWay" next ctx let! _ = withHxTriggerAfterSettle "byTheWay" next ctx
Expect.isTrue Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present" Expect.equal dic["HX-Trigger"].[0] "byTheWay" "The HX-Trigger value was incorrect"
Expect.equal dic["HX-Trigger-After-Settle"].[0] "byTheWay" "The HX-Trigger-After-Settle value was incorrect"
} }
testTask "withHxTriggerManyAfterSettle succeeds" { testTask "withHxTriggerManyAfterSettle succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx let! _ = withHxTriggerManyAfterSettle [ "oof", "ouch"; "hmm", "uh" ] next ctx
Expect.isTrue Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
(dic.ContainsKey "HX-Trigger-After-Settle") "The HX-Trigger-After-Settle header should be present" Expect.equal dic["HX-Trigger"].[0] """{ "oof": "ouch", "hmm": "uh" }""" "The HX-Trigger value was incorrect"
Expect.equal
dic["HX-Trigger-After-Settle"].[0] """{ "oof": "ouch", "hmm": "uh" }"""
"The HX-Trigger-After-Settle value was incorrect"
} }
testTask "withHxTriggerAfterSwap succeeds" { testTask "withHxTriggerAfterSwap succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerAfterSwap "justASec" next ctx let! _ = withHxTriggerAfterSwap "justASec" next ctx
Expect.isTrue (dic.ContainsKey "HX-Trigger-After-Swap") "The HX-Trigger-After-Swap header should be present" Expect.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal dic["HX-Trigger-After-Swap"].[0] "justASec" "The HX-Trigger-After-Swap value was incorrect" Expect.equal dic["HX-Trigger"].[0] "justASec" "The HX-Trigger value was incorrect"
} }
testTask "withHxTriggerManyAfterSwap succeeds" { testTask "withHxTriggerManyAfterSwap succeeds" {
let ctx = Substitute.For<HttpContext> () let ctx = Substitute.For<HttpContext>()
let dic = HeaderDictionary () let dic = HeaderDictionary()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
let! _ = withHxTriggerManyAfterSwap [ "this", "1"; "that", "2" ] next ctx 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.isTrue (dic.ContainsKey "HX-Trigger") "The HX-Trigger header should be present"
Expect.equal Expect.equal dic["HX-Trigger"].[0] """{ "this": "1", "that": "2" }""" "The HX-Trigger value was incorrect"
dic["HX-Trigger-After-Swap"].[0] """{ "this": "1", "that": "2" }"""
"The HX-Trigger-After-Swap value was incorrect"
} }
] ]
/// All tests for this module /// All tests for this module
let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers ] let allTests = testList "Htmx" [ dictExtensions; reqExtensions; handlers; script; dictExtensionsObs; handlerObs ]
+2 -2
View File
@@ -1,6 +1,6 @@
open Expecto open Expecto
let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests ] let allTests = testList "Giraffe" [ Common.allTests; Htmx.allTests; ViewEngine.allTests; ViewEngineMax.allTests ]
[<EntryPoint>] [<EntryPoint>]
let main args = runTestsWithArgs defaultConfig args allTests let main args = runTestsWithCLIArgs [] args allTests
+4 -2
View File
@@ -8,6 +8,7 @@
<Compile Include="Common.fs" /> <Compile Include="Common.fs" />
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<Compile Include="ViewEngine.fs" /> <Compile Include="ViewEngine.fs" />
<Compile Include="ViewEngineMax.fs" />
<Compile Include="Program.fs" /> <Compile Include="Program.fs" />
</ItemGroup> </ItemGroup>
@@ -18,8 +19,9 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Expecto" Version="9.0.4" /> <PackageReference Include="Expecto" />
<PackageReference Include="NSubstitute" Version="5.0.0" /> <PackageReference Include="NSubstitute" />
<PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
</Project> </Project>
+881 -122
View File
File diff suppressed because it is too large Load Diff
+335
View File
@@ -0,0 +1,335 @@
module ViewEngineMax
open Expecto
open Giraffe.ViewEngine.Htmax
/// Tests for the HxBrowserIndicatorConfigItem type
let hxBrowserIndicatorConfigItem =
testList "HxBrowserIndicatorConfigItem" [
testList "ToHcon" [
test "succeeds for BoostBrowserIndicator" {
Expect.equal
((BoostBrowserIndicator false).ToHcon())
"boostBrowserIndicator:false"
"The HCON value was incorrect"
}
]
]
/// Tests for the HxPreloadConfigItem type
let hxPreloadConfigItem =
testList "HxPreloadConfigItem" [
testList "ToHcon" [
test "succeeds for AutoBoost" {
Expect.equal ((AutoBoost false).ToHcon()) "autoBoost:false" "The HCON value was incorrect"
}
test "succeeds for BoostEvent" {
Expect.equal ((BoostEvent "mouseover").ToHcon()) "boostEvent:mouseover" "The HCON value was incorrect"
}
test "succeeds for BoostTimeout" {
Expect.equal ((BoostTimeout 30_000).ToHcon()) "boostTimeout:30000" "The HCON value was incorrect"
}
]
]
/// Tests for the HxSseWsConfigItem type
let hxSseWsConfigItem =
testList "HxSseWsConfigItem" [
testList "ToHcon" [
test "succeeds for Reconnect" {
Expect.equal ((Reconnect true).ToHcon()) "reconnect:true" "The HCON value was incorrect"
}
testList "ReconnectDelay" [
test "succeeds for numeric value" {
Expect.equal ((ReconnectDelay "3000").ToHcon()) "reconnectDelay:3000" "The HCON value was incorrect"
}
test "succeeds for value with units" {
Expect.equal ((ReconnectDelay "3s").ToHcon()) "reconnectDelay:3s" "The HCON value was incorrect"
}
]
testList "ReconnectMaxDelay" [
test "succeeds for numeric value" {
Expect.equal
((ReconnectMaxDelay "30000").ToHcon()) "reconnectMaxDelay:30000" "The HCON value was incorrect"
}
test "succeeds for value with units" {
Expect.equal
((ReconnectMaxDelay "50s").ToHcon()) "reconnectMaxDelay:50s" "The HCON value was incorrect"
}
]
testList "ReconnectMaxAttempts" [
test "succeeds for negative 1" {
Expect.equal
((ReconnectMaxAttempts -1).ToHcon())
"reconnectMaxAttempts:Infinity"
"The HCON value was incorrect"
}
test "succeeds for zero" {
Expect.equal
((ReconnectMaxAttempts 0).ToHcon()) "reconnectMaxAttempts:0" "The HCON value was incorrect"
}
test "succeeds for positive number" {
Expect.equal
((ReconnectMaxAttempts 7).ToHcon()) "reconnectMaxAttempts:7" "The HCON value was incorrect"
}
]
test "succeeds for ReconnectJitter" {
Expect.equal ((ReconnectJitter 0.7).ToHcon()) "reconnectJitter:0.7" "The HCON value was incorrect"
}
test "succeeds for PauseOnBackground" {
Expect.equal
((PauseOnBackground false).ToHcon()) "pauseOnBackground:false" "The HCON value was incorrect"
}
test "succeeds for PendingRequestTtl" {
Expect.equal ((PendingRequestTtl 5000).ToHcon()) "pendingRequestTTL:5000" "The HCON value was incorrect"
}
]
]
/// Tests for the HxConfig module
let hxConfig =
testList "HxConfig" [
test "hxBrowserIndicatorConfig succeeds" {
Expect.equal
(hxBrowserIndicatorConfig [ BoostBrowserIndicator true ])
"browser-indicator.boostBrowserIndicator:true"
"Config settings not correct"
}
test "hxPreloadConfig succeeds" {
Expect.equal
(hxPreloadConfig [ AutoBoost true; BoostTimeout 3_000 ])
"preload.autoBoost:true preload.boostTimeout:3000"
"Config settings not correct"
}
test "hxSseConfig succeeds" {
Expect.equal
(hxSseConfig [ ReconnectDelay "5s"; PauseOnBackground false ])
"sse.reconnectDelay:5s sse.pauseOnBackground:false"
"Config settings not correct"
}
test "hxWsConfig succeeds" {
Expect.equal
(hxWsConfig [ Reconnect false; PendingRequestTtl 40_000 ])
"ws.reconnect:false ws.pendingRequestTTL:40000"
"Config settings not correct"
}
]
/// Tests for the HxEvent module
let hxEvent =
testList "HxEvent" [
testList "AfterSseConnection" [
test "ToString succeeds" {
Expect.equal
(string AfterSseConnection) "afterSseConnection" "AfterSseConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseConnection.ToHxOnString())
"after:sse:connection"
"AfterSseConnection hx-on event name not correct"
}
]
testList "AfterSseMessage" [
test "ToString succeeds" {
Expect.equal (string AfterSseMessage) "afterSseMessage" "AfterSseMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterSseMessage.ToHxOnString()) "after:sse:message" "AfterSseMessage hx-on event name not correct"
}
]
testList "AfterWsConnection" [
test "ToString succeeds" {
Expect.equal
(string AfterWsConnection) "afterWsConnection" "AfterWsConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsConnection.ToHxOnString())
"after:ws:connection"
"AfterWsConnection hx-on event name not correct"
}
]
testList "AfterWsMessage" [
test "ToString succeeds" {
Expect.equal (string AfterWsMessage) "afterWsMessage" "AfterWsMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsMessage.ToHxOnString()) "after:ws:message" "AfterWsMessage hx-on event name not correct"
}
]
testList "AfterWsRequest" [
test "ToString succeeds" {
Expect.equal (string AfterWsRequest) "afterWsRequest" "AfterWsRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(AfterWsRequest.ToHxOnString()) "after:ws:request" "AfterWsRequest hx-on event name not correct"
}
]
testList "BeforeSseConnection" [
test "ToString succeeds" {
Expect.equal
(string BeforeSseConnection) "beforeSseConnection" "BeforeSseConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseConnection.ToHxOnString())
"before:sse:connection"
"BeforeSseConnection hx-on event name not correct"
}
]
testList "BeforeSseMessage" [
test "ToString succeeds" {
Expect.equal (string BeforeSseMessage) "beforeSseMessage" "BeforeSseMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeSseMessage.ToHxOnString())
"before:sse:message"
"BeforeSseMessage hx-on event name not correct"
}
]
testList "BeforeWsConnection" [
test "ToString succeeds" {
Expect.equal
(string BeforeWsConnection) "beforeWsConnection" "BeforeWsConnection event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsConnection.ToHxOnString())
"before:ws:connection"
"BeforeWsConnection hx-on event name not correct"
}
]
testList "BeforeWsMessage" [
test "ToString succeeds" {
Expect.equal (string BeforeWsMessage) "beforeWsMessage" "BeforeWsMessage event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsMessage.ToHxOnString()) "before:ws:message" "BeforeWsMessage hx-on event name not correct"
}
]
testList "BeforeWsRequest" [
test "ToString succeeds" {
Expect.equal (string BeforeWsRequest) "beforeWsRequest" "BeforeWsRequest event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(BeforeWsRequest.ToHxOnString()) "before:ws:request" "BeforeWsRequest hx-on event name not correct"
}
]
testList "DownloadComplete" [
test "ToString succeeds" {
Expect.equal (string DownloadComplete) "downloadComplete" "DownloadComplete event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadComplete.ToHxOnString())
"download:complete"
"DownloadComplete hx-on event name not correct"
}
]
testList "DownloadProgress" [
test "ToString succeeds" {
Expect.equal (string DownloadProgress) "downloadProgress" "DownloadProgress event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadProgress.ToHxOnString())
"download:progress"
"DownloadProgress hx-on event name not correct"
}
]
testList "DownloadStart" [
test "ToString succeeds" {
Expect.equal (string DownloadStart) "downloadStart" "DownloadStart event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal
(DownloadStart.ToHxOnString()) "download:start" "DownloadStart hx-on event name not correct"
}
]
testList "SseClose" [
test "ToString succeeds" {
Expect.equal (string SseClose) "sseClose" "SseClose event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SseClose.ToHxOnString()) "sse:close" "SseClose hx-on event name not correct"
}
]
testList "SseError" [
test "ToString succeeds" {
Expect.equal (string SseError) "sseError" "SseError event name not correct"
}
test "ToHxOnString succeeds" {
Expect.equal (SseError.ToHxOnString()) "sse:error" "SseError hx-on event name not correct"
}
]
]
open Giraffe.ViewEngine
/// Pipe-able assertion for a rendered node
let shouldRender expected node =
Expect.equal (RenderView.AsString.htmlNode node) expected "Rendered HTML incorrect"
/// Tests for the HtmaxAttrs module
let attributes =
testList "Attributes" [
test "_hxBrowserIndicator succeeds" {
a [ _hxBrowserIndicator ] [] |> shouldRender """<a hx-browser-indicator="true"></a>"""
}
test "_hxLive succeeds" {
main [ _hxLive "q().doStuff()" ] [] |> shouldRender """<main hx-live="q().doStuff()"></main>"""
}
test "_hxOnMax succeeds" {
body [ _hxOnMax SseClose "alert(done)" ] []
|> shouldRender """<body hx-on:htmx:sse:close="alert(done)"></body>"""
}
testList "_hxPreload" [
test "succeeds when given an event" {
blockquote [ _hxPreload (Some "focus") ] []
|> shouldRender """<blockquote hx-preload="focus"></blockquote>"""
}
test "succeeds when not given an event" {
em [ _hxPreload None ] [] |> shouldRender """<em hx-preload></em>"""
}
]
test "_hxSseClose succeeds" {
form [ _hxSseClose "fin" ] [] |> shouldRender """<form hx-sse:close="fin"></form>"""
}
test "_hxSseConnect succeeds" {
div [ _hxSseConnect "/path/to/resource" ] []
|> shouldRender """<div hx-sse:connect="/path/to/resource"></div>"""
}
test "_hxTargets succeeds" {
dl [ _hxTargets ".ephemeral" ] [] |> shouldRender """<dl hx-targets=".ephemeral"></dl>"""
}
test "_hxWsConnect succeeds" {
p [ _hxWsConnect "/over/here" ] [] |> shouldRender """<p hx-ws:connect="/over/here"></p>"""
}
testList "_hxWsSend" [
test "succeeds when given a URL" {
span [ _hxWsSend (Some "/a/new/place") ] []
|> shouldRender """<span hx-ws:send="/a/new/place"></span>"""
}
test "succeeds when not given a URL" {
aside [ _hxWsSend None ] [] |> shouldRender """<aside hx-ws:send></aside>"""
}
]
]
let allTests =
testList "ViewEngine.Htmax" [
hxBrowserIndicatorConfigItem
hxPreloadConfigItem
hxSseWsConfigItem
hxConfig
hxEvent
attributes
]
@@ -4,15 +4,19 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description> <Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<DocumentationFile>bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Compile Include="Htmx.fs" /> <Compile Include="Htmx.fs" />
<Compile Include="Htmax.fs" />
<None Include="README.md" Pack="true" PackagePath="\" /> <None Include="README.md" Pack="true" PackagePath="\" />
<None Include="..\icon.png" Pack="true" PackagePath="\" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Giraffe.ViewEngine" Version="1.4.0" /> <PackageReference Include="Giraffe.ViewEngine" />
<PackageReference Include="FSharp.Core" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+243
View File
@@ -0,0 +1,243 @@
/// <summary>Types and functions supporting htmax-bundled extension attributes in Giraffe View Engine</summary>
module Giraffe.ViewEngine.Htmax
/// <summary>Items which can be configured for the <c>hx-browser-indicator</c> extension</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#boosted-elements">Documentation</seealso>
type HxBrowserIndicatorConfigItem =
/// <summary>Whether to show the browser's native loading indicator for <c>hx-boost</c>ed links</summary>
| BoostBrowserIndicator of bool
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| BoostBrowserIndicator it -> "boostBrowserIndicator:" + (string it).ToLowerInvariant()
/// <summary>Items which can be configured for the <c>hx-preload</c> extension</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-preload#configuration">Documentation</seealso>
type HxPreloadConfigItem =
/// <summary>Whether items subject to <c>hx-boost</c> are automatically preloaded (default <c>true</c>)</summary>
| AutoBoost of bool
/// <summary>The event to use for <c>hx-boost</c> auto-preloaded content (default "mousedown")</summary>
| BoostEvent of string
/// <summary>The timeout (in ms) for <c>hx-boost</c> auto-preloaded requests (default 50_000)</summary>
| BoostTimeout of int
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| AutoBoost it -> "autoBoost:" + (string it).ToLowerInvariant()
| BoostEvent it -> $"boostEvent:{it}"
| BoostTimeout it -> $"boostTimeout:{it}"
/// <summary>Items which can be configured for the <c>hx-sse</c> or <c>hx-ws</c> extensions</summary>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#configuration">SSE Documentation</seealso>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#configuration">WebSockets Documentation</seealso>
type HxSseWsConfigItem =
/// <summary>Whether to automatically reconnect on stream end (default <c>true</c>)</summary>
| Reconnect of bool
/// <summary>Delay for reconnect attempts (ms, or numbers with units "1s", "2m"; default "500")</summary>
| ReconnectDelay of string
/// <summary>Maximum delay for reconnect attempts (ms, or numbers with units "1s, "2m"; default "60000")</summary>
| ReconnectMaxDelay of string
/// <summary>Reconnect maximum attempts (use -1 for Infinity; default Infinity)</summary>
| ReconnectMaxAttempts of int
/// <summary>Jitter to use when reconnecting (decimal between 0.0 and 1.0; default 0.3)</summary>
| ReconnectJitter of double
/// <summary>Whether to pause when the current tab is in the background (default <c>true</c>)</summary>
| PauseOnBackground of bool
/// <summary>Time-to-Live (TTL) for pending requests (<c>hx-ws</c> only)</summary>
| PendingRequestTtl of int
/// <summary>Get the HCON representation of this configuration value</summary>
member this.ToHcon() =
match this with
| Reconnect it -> "reconnect:" + (string it).ToLowerInvariant()
| ReconnectDelay it -> $"reconnectDelay:{it}"
| ReconnectMaxDelay it -> $"reconnectMaxDelay:{it}"
| ReconnectMaxAttempts it -> "reconnectMaxAttempts:" + if it = -1 then "Infinity" else string it
| ReconnectJitter it -> $"reconnectJitter:{it}"
| PauseOnBackground it -> "pauseOnBackground:" + (string it).ToLowerInvariant()
| PendingRequestTtl it -> $"pendingRequestTTL:{it}"
/// <summary>Helpers for generating extension configuration parameters</summary>
[<AutoOpen>]
module HxConfig =
/// Create an HCON section suitable for hx-config
let private hconSettings title values =
values |> Seq.map (sprintf "%s.%s" title) |> String.concat " "
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxBrowserIndicatorConfig (items: HxBrowserIndicatorConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "browser-indicator"
/// <summary>Generate configuration items for preload suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxPreloadConfig (items: HxPreloadConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "preload"
/// <summary>Generate configuration items for SSE suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxSseConfig (items: HxSseWsConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "sse"
/// <summary>Generate configuration items for WebSockets suitable for <c>hx-config</c></summary>
/// <param name="items">The configuration items</param>
let hxWsConfig (items: HxSseWsConfigItem seq) =
items |> Seq.map _.ToHcon() |> hconSettings "ws"
/// <summary>The events recognized by htmax-bundled extensions</summary>
[<Struct>]
type HxEvent =
/// <summary>Fired after an SSE connection is established</summary>
| AfterSseConnection
/// <summary>Fired after an SSE message is received</summary>
| AfterSseMessage
/// <summary>Fired after a WebSocket connection is established</summary>
| AfterWsConnection
/// <summary>Fired after a WebSocket message is received</summary>
| AfterWsMessage
/// <summary>Fired after a WebSocket request is sent</summary>
| AfterWsRequest
/// <summary>Fired before an SSE connection is established (cancelable)</summary>
| BeforeSseConnection
/// <summary>Fired before a received SSE message is processed (cancelable)</summary>
| BeforeSseMessage
/// <summary>Fired before a WebSocket connection is established (cancelable)</summary>
| BeforeWsConnection
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
| DownloadComplete
/// <summary>Fired when a download handled by <c>hx-download</c> is complete</summary>
| DownloadProgress
/// <summary>Fired when a chunk is received for a download handled by <c>hx-download</c></summary>
| DownloadStart
/// <summary>Fired before a received WebSocket message is processed (cancelable)</summary>
| BeforeWsMessage
/// <summary>Fired before a WebSocket request is sent (cancelable)</summary>
| BeforeWsRequest
/// <summary>Fired when an SSE connection is closed</summary>
| SseClose
/// <summary>Fired when an SSE connection errors</summary>
| SseError
/// The htmx event name (fst) and kebab-case name (snd, for use with hx-on)
static member private Values = Map [
AfterSseConnection, ("afterSseConnection", "after:sse:connection")
AfterSseMessage, ("afterSseMessage", "after:sse:message")
AfterWsConnection, ("afterWsConnection", "after:ws:connection")
AfterWsMessage, ("afterWsMessage", "after:ws:message")
AfterWsRequest, ("afterWsRequest", "after:ws:request")
BeforeSseConnection, ("beforeSseConnection", "before:sse:connection")
BeforeSseMessage, ("beforeSseMessage", "before:sse:message")
BeforeWsConnection, ("beforeWsConnection", "before:ws:connection")
BeforeWsMessage, ("beforeWsMessage", "before:ws:message")
BeforeWsRequest, ("beforeWsRequest", "before:ws:request")
DownloadComplete, ("downloadComplete", "download:complete")
DownloadProgress, ("downloadProgress", "download:progress")
DownloadStart, ("downloadStart", "download:start")
SseClose, ("sseClose", "sse:close")
SseError, ("sseError", "sse:error")
]
/// <summary>The htmx event name</summary>
override this.ToString() = fst HxEvent.Values[this]
/// <summary>The <c>hx-on</c> variant of the htmx event name</summary>
member this.ToHxOnString() = snd HxEvent.Values[this]
/// <summary>Attributes and flags for htmax-bundled extensions</summary>
[<AutoOpen>]
module HtmaxAttrs =
/// <summary>Display the browser's native loading indicator when an htmx request is in flight</summary>
/// <returns>A configured <c>hx-browser-indicator</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-browser-indicator#usage">Documentation</seealso>
let _hxBrowserIndicator =
attr "hx-browser-indicator" "true"
/// <summary>Run script each time the DOM changes</summary>
/// <param name="script">The script to be run each time the DOM changes (may use <c>q</c> helper)</param>
/// <returns>A configured <c>hx-live</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-live#the-hx-live-attribute">Documentation</seealso>
let _hxLive script =
attr "hx-live" script
/// <summary>Generate an <c>hx-on:htmx</c> event for an htmax-bundled extension event</summary>
/// <param name="event">The <c>HxEvent</c> to be handled</param>
/// <param name="handler">The script to be executed when the event occurs</param>
/// <returns>A configured <c>hx-on:htmx:</c> attribute</returns>
/// <seealso href="https://htmx.org/attributes/hx-on/">Documentation</seealso>
let _hxOnMax (event: HxEvent) handler =
Htmx.HtmxAttrs._hxOnEvent $"htmx:{event.ToHxOnString()}" handler
/// <summary>Identify a resource as one that should be preloaded</summary>
/// <param name="evt">The DOM event or htmx trigger which should cause the preload (optional; default behavior is
/// preloading on <c>mousedown</c>)</param>
/// <returns>A configured <c>hx-preload</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-preload#usage">Documentation</seealso>
let _hxPreload evt =
match evt with Some it -> attr "hx-preload" it | None -> flag "hx-preload"
/// <summary>Define an SSE message which indicates the connection should be closed</summary>
/// <param name="message">The text of the message which indicates the SSE connection should be closed</param>
/// <returns>A configured <c>hx-sse:close</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseclose">Documentation</seealso>
let _hxSseClose message =
attr "hx-sse:close" message
/// <summary>Define a URL to which an SSE connection should be established</summary>
/// <param name="url">The URL to which an SSE connection should be established</param>
/// <returns>A configured <c>hx-sse:connect</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-sse#hx-sseconnect">Documentation</seealso>
let _hxSseConnect url =
attr "hx-sse:connect" url
/// <summary>Replace multiple targets with the same content</summary>
/// <param name="selector">The CSS selector to identify targets for the replacement</param>
/// <returns>A configured <c>hx-targets</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-targets#usage">Documentation</seealso>
let _hxTargets selector =
attr "hx-targets" selector
/// <summary>Connect to a WebSocket URL</summary>
/// <param name="url">The URL to which a WebSocket connection should be established</param>
/// <returns>A configured <c>hx-ws:connect</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
let _hxWsConnect url =
attr "hx-ws:connect" url
/// <summary>Send data via a WebSocket connection</summary>
/// <param name="url">The URL to which the data should be sent (optional; default is connection URL)</param>
/// <returns>A configured <c>hx-ws:send</c> attribute</returns>
/// <seealso href="https://four.htmx.org/extensions/hx-ws#usage">Documentation</seealso>
let _hxWsSend url =
match url with Some it -> attr "hx-ws:send" it | None -> flag "hx-ws:send"
File diff suppressed because it is too large Load Diff
+26 -16
View File
@@ -2,32 +2,42 @@
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine. This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
**htmx version: 1.9.8** **htmx version: 4.0.0-beta4**
_Upgrading from v2.x: see [the migration guide](https://four.htmx.org/docs/get-started/migration) for changes, which are plentiful. htmx switches from `XMLHTTPRequest` to `fetch`, and many changes are related to the new event cycle._
_Inheritance is now explicit; to have an attribute's value inherited to its children, wrap the attribute in `hxInherited` (ex. `hxInherited (_hxTarget "#main")`). Values can be appended to inherited values as well using the `hxAppend` modifier._
_Several constructs have been marked obsolete in this release, and will be removed from the first production release of v4. With the exception of `_hxDisable`, though (which now functions as the deprecated `_hxDisabledElt` did), this should not introduce compile errors. Rather, this package will raise warnings for deprecated constructs, along with suggestions of what to use instead._
### Setup ### Setup
1. Install the package. 1. Install the package.
2. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`. 1. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`.
1. If the client is using the `htmax` bundle, also `open Giraffe.ViewEngine.Htmax`.
### Use ### Use
Following Giraffe View Engine's lead, there are a set of attribute functions for htmx; for many of the attributes, there are also helper modules to assist with typing the values. The example below utilizes both: Following Giraffe View Engine's lead, there are a set of attribute functions for htmx; for many of the attributes, there are also helper modules to assist with typing the values. The example below utilizes both:
```fsharp ```fsharp
let autoload = let autoload =
div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ] div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ]
``` ```
Support modules include: Support modules include:
- `HxConfig` _(new in v4)_
- `HxEncoding` - `HxEncoding`
- `HxHeaders` - `HxHeaders`
- `HxParams` - ~~`HxParams`~~ _(removed in v4)_
- `HxRequest` - ~~`HxRequest`~~ _(renamed to `HxConfig`)_
- `HxSwap` (requires `open Giraffe.Htmx`) - `HxSwap` (requires `open Giraffe.Htmx`)
- `HxTrigger` - `HxTrigger`
- `HxVals` - `HxVals`
There are two `XmlNode`s that will load the htmx script from unpkg; `Htmx.Script.minified` loads the minified version, and `Htmx.Script.unminified` loads the unminified version (useful for debugging). `Htmx.Script.local` creates an `XmlNode` to load the package-provided htmx library. There are also two `XmlNode`s that will load the htmx script from jsdelivr; `Htmx.Script.cdnMinified` loads the minified version, and `Htmx.Script.cdnUnminified` loads the unminified version (useful for debugging). htmx v4 also distributes a "max" bundle which contains some common extensions; these are available with the same names under `Htmx.Script.Max`.
_NOTE: When using the CDN nodes and a Content Security Policy (CSP) header, `cdn.jsdelivr.net` needs to be listed as an allowable `script-src`._
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute). This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).
@@ -40,22 +50,22 @@ htmx's attributes and these attribute functions map one-to-one. There are two ex
The htmx `hx-on` attribute supports multiple events if they are separated with a newline (`\n`) character. The value provided to this attribute will be attribute-escaped, but in testing, it was interpreted correctly. The htmx `hx-on` attribute supports multiple events if they are separated with a newline (`\n`) character. The value provided to this attribute will be attribute-escaped, but in testing, it was interpreted correctly.
The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though: The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though:
- `HxRequest` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request. - `HxConfig` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request.
```fsharp ```fsharp
HxRequest.Configure [ HxRequest.Timeout 500 ] |> _hxRequest HxConfig.Configure [ HxRequest.Timeout 500 ] |> _hxConfig
``` ```
- `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing: - `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing:
```fsharp ```fsharp
HxTrigger.Click HxTrigger.Click
|> HxTrigger.Filter.Shift |> HxTrigger.Filter.Shift
|> HxTrigger.FromDocument |> HxTrigger.FromDocument
|> HxTrigger.Delay "3s" |> HxTrigger.Delay "3s"
|> _hxTrigger |> _hxTrigger
// or // or
(HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click (HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click
|> _hxTrigger |> _hxTrigger
``` ```
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

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