Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c587a28770 | |||
| b5292bffc4 | |||
| 86defea3c1 | |||
| 9a9f159cab | |||
| 9fcba06e75 | |||
| 5906f3b295 | |||
| dc06b06b1f |
@@ -63,6 +63,8 @@ Some attributes have known values, such as `hx-trigger` and `hx-swap`; for these
|
|||||||
]
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to load htmx from unpkg, `Htmx.Script.minified` or `Htmx.Script.unminified` can be used to load the script in your HTML trees.
|
||||||
|
|
||||||
## Feedback / Help
|
## Feedback / Help
|
||||||
|
|
||||||
The author hangs out in the #htmx-general channel of the [htmx Discord server](https://htmx.org/discord) and the #web channel of the [F# Software Foundation's Slack server](https://fsharp.org/guides/slack/).
|
The author hangs out in the #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/).
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
<?xml version="1.0" encoding="utf-8" standalone="no"?>
|
||||||
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<VersionPrefix>0.9.1</VersionPrefix>
|
<VersionPrefix>1.7.0</VersionPrefix>
|
||||||
<PackageReleaseNotes>Initial NuGet release</PackageReleaseNotes>
|
<PackageReleaseNotes>Support new attributes/headers in htmx 1.7.0</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://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl>
|
||||||
|
|||||||
@@ -217,6 +217,17 @@ module HandlerTests =
|
|||||||
Assert.Equal ("/a-new-url", dic.["HX-Push"].[0])
|
Assert.Equal ("/a-new-url", dic.["HX-Push"].[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``withHxNoPush succeeds`` () =
|
||||||
|
let ctx = Substitute.For<HttpContext> ()
|
||||||
|
let dic = HeaderDictionary ()
|
||||||
|
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
task {
|
||||||
|
let! _ = withHxNoPush next ctx
|
||||||
|
Assert.True (dic.ContainsKey "HX-Push")
|
||||||
|
Assert.Equal ("false", dic.["HX-Push"].[0])
|
||||||
|
}
|
||||||
|
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
let ``withHxRedirect succeeds`` () =
|
let ``withHxRedirect succeeds`` () =
|
||||||
let ctx = Substitute.For<HttpContext> ()
|
let ctx = Substitute.For<HttpContext> ()
|
||||||
@@ -250,6 +261,17 @@ module HandlerTests =
|
|||||||
Assert.Equal ("false", dic.["HX-Refresh"].[0])
|
Assert.Equal ("false", dic.["HX-Refresh"].[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``withHxRetarget succeeds`` () =
|
||||||
|
let ctx = Substitute.For<HttpContext> ()
|
||||||
|
let dic = HeaderDictionary ()
|
||||||
|
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
|
||||||
|
task {
|
||||||
|
let! _ = withHxRetarget "#somewhereElse" next ctx
|
||||||
|
Assert.True (dic.ContainsKey "HX-Retarget")
|
||||||
|
Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0])
|
||||||
|
}
|
||||||
|
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
let ``withHxTrigger succeeds`` () =
|
let ``withHxTrigger succeeds`` () =
|
||||||
let ctx = Substitute.For<HttpContext> ()
|
let ctx = Substitute.For<HttpContext> ()
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<Description>htmx header extensions and helpers for Giraffe</Description>
|
<Description>htmx header extensions and helpers for Giraffe</Description>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Htmx.fs" />
|
<Compile Include="Htmx.fs" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ type IHeaderDictionary with
|
|||||||
/// Extensions for the request object
|
/// Extensions for the request object
|
||||||
type HttpRequest with
|
type HttpRequest with
|
||||||
|
|
||||||
/// Whether this request was initiated from HTMX
|
/// Whether this request was initiated from htmx
|
||||||
member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false
|
member this.IsHtmx with get () = this.Headers.HxRequest |> Option.defaultValue false
|
||||||
|
|
||||||
/// Whether this request is an HTMX history-miss refresh request
|
/// Whether this request is an htmx history-miss refresh request
|
||||||
member this.IsHtmxRefresh with get () =
|
member this.IsHtmxRefresh with get () =
|
||||||
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
|
this.IsHtmx && (this.Headers.HxHistoryRestoreRequest |> Option.defaultValue false)
|
||||||
|
|
||||||
@@ -66,6 +66,10 @@ module Handlers =
|
|||||||
let withHxPush : string -> HttpHandler =
|
let withHxPush : string -> HttpHandler =
|
||||||
setHttpHeader "HX-Push"
|
setHttpHeader "HX-Push"
|
||||||
|
|
||||||
|
// Explicitly do not push a new URL into the history stack
|
||||||
|
let withHxNoPush : HttpHandler =
|
||||||
|
toLowerBool false |> withHxPush
|
||||||
|
|
||||||
/// Can be used to do a client-side redirect to a new location
|
/// Can be used to do a client-side redirect to a new location
|
||||||
let withHxRedirect : string -> HttpHandler =
|
let withHxRedirect : string -> HttpHandler =
|
||||||
setHttpHeader "HX-Redirect"
|
setHttpHeader "HX-Redirect"
|
||||||
@@ -74,6 +78,10 @@ module Handlers =
|
|||||||
let withHxRefresh : bool -> HttpHandler =
|
let withHxRefresh : bool -> HttpHandler =
|
||||||
toLowerBool >> setHttpHeader "HX-Refresh"
|
toLowerBool >> setHttpHeader "HX-Refresh"
|
||||||
|
|
||||||
|
/// Allows you to override the `hx-target` attribute
|
||||||
|
let withHxRetarget : string -> HttpHandler =
|
||||||
|
setHttpHeader "HX-Retarget"
|
||||||
|
|
||||||
/// Allows you to trigger a single client side event
|
/// Allows you to trigger a single client side event
|
||||||
let withHxTrigger : string -> HttpHandler =
|
let withHxTrigger : string -> HttpHandler =
|
||||||
setHttpHeader "HX-Trigger"
|
setHttpHeader "HX-Trigger"
|
||||||
|
|||||||
35
src/Htmx/README.md
Normal file
35
src/Htmx/README.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
## Giraffe.Htmx
|
||||||
|
|
||||||
|
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
|
||||||
|
|
||||||
|
**htmx version: 1.7.0**
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install the package.
|
||||||
|
2. Prior to using the request header extension properties or the header-setting `HttpHandler`s, `open Giraffe.Htmx`.
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
To obtain a request header, using the `IHeaderDictionary` extension properties:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let myHandler : HttpHander =
|
||||||
|
fun next ctx ->
|
||||||
|
match ctx.HxPrompt with
|
||||||
|
| Some prompt -> ... // do something with the text the user provided
|
||||||
|
| None -> ... // no text provided
|
||||||
|
```
|
||||||
|
|
||||||
|
To set a response header:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let myHandler : HttpHander =
|
||||||
|
fun next ctx ->
|
||||||
|
// some meaningful work
|
||||||
|
withHxPush "/some/new/url" >=> [other handlers]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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.
|
||||||
@@ -15,6 +15,19 @@ module Encoding =
|
|||||||
Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm)
|
Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm)
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests for the HxHeaders module
|
||||||
|
module Headers =
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds with an empty list`` () =
|
||||||
|
Assert.Equal ("{ }", HxHeaders.From [])
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds and escapes quotes`` () =
|
||||||
|
Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }",
|
||||||
|
HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ])
|
||||||
|
|
||||||
|
|
||||||
/// Tests for the HxParams module
|
/// Tests for the HxParams module
|
||||||
module Params =
|
module Params =
|
||||||
|
|
||||||
@@ -51,6 +64,43 @@ module Params =
|
|||||||
Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ])
|
Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ])
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests for the HxRequest module
|
||||||
|
module Request =
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Configure succeeds with an empty list`` () =
|
||||||
|
Assert.Equal ("{ }", HxRequest.Configure [])
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Configure succeeds with a non-empty list`` () =
|
||||||
|
Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ])
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Configure succeeds with all known params configured`` () =
|
||||||
|
Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }",
|
||||||
|
HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Timeout succeeds`` () =
|
||||||
|
Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Credentials succeeds when set to true`` () =
|
||||||
|
Assert.Equal ("\"credentials\": true", HxRequest.Credentials true)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Credentials succeeds when set to false`` () =
|
||||||
|
Assert.Equal ("\"credentials\": false", HxRequest.Credentials false)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``NoHeaders succeeds when set to true`` () =
|
||||||
|
Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``NoHeaders succeeds when set to false`` () =
|
||||||
|
Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false)
|
||||||
|
|
||||||
|
|
||||||
/// Tests for the HxSwap module
|
/// Tests for the HxSwap module
|
||||||
module Swap =
|
module Swap =
|
||||||
|
|
||||||
@@ -94,6 +144,14 @@ module Trigger =
|
|||||||
let ``Load is correct`` () =
|
let ``Load is correct`` () =
|
||||||
Assert.Equal ("load", HxTrigger.Load)
|
Assert.Equal ("load", HxTrigger.Load)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Revealed is correct`` () =
|
||||||
|
Assert.Equal ("revealed", HxTrigger.Revealed)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Every succeeds`` () =
|
||||||
|
Assert.Equal ("every 3s", HxTrigger.Every "3s")
|
||||||
|
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
let ``Filter.Alt succeeds`` () =
|
let ``Filter.Alt succeeds`` () =
|
||||||
Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click)
|
Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click)
|
||||||
@@ -122,6 +180,147 @@ module Trigger =
|
|||||||
let ``Filter.AltShift succeeds`` () =
|
let ``Filter.AltShift succeeds`` () =
|
||||||
Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click)
|
Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Once succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("once", HxTrigger.Once "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Once succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click once", HxTrigger.Once "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Changed succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("changed", HxTrigger.Changed "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Changed succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click changed", HxTrigger.Changed "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Delay succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Delay succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Throttle succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Throttle succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("from:.nav", HxTrigger.From ".nav" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromDocument succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("from:document", HxTrigger.FromDocument "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromDocument succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click from:document", HxTrigger.FromDocument "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromWindow succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("from:window", HxTrigger.FromWindow "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromWindow succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click from:window", HxTrigger.FromWindow "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromClosest succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromClosest succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromFind succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("from:find li", HxTrigger.FromFind "li" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``FromFind succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Target succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("target:main", HxTrigger.Target "main" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Target succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Consume succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("consume", HxTrigger.Consume "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Consume succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click consume", HxTrigger.Consume "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Queue succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Queue succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueFirst succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("queue:first", HxTrigger.QueueFirst "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueFirst succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueLast succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("queue:last", HxTrigger.QueueLast "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueLast succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click queue:last", HxTrigger.QueueLast "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueAll succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("queue:all", HxTrigger.QueueAll "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueAll succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click queue:all", HxTrigger.QueueAll "click")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueNone succeeds when it is the first modifier`` () =
|
||||||
|
Assert.Equal ("queue:none", HxTrigger.QueueNone "")
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``QueueNone succeeds when it is not the first modifier`` () =
|
||||||
|
Assert.Equal ("click queue:none", HxTrigger.QueueNone "click")
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests for the HxVals module
|
||||||
|
module Vals =
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds with an empty list`` () =
|
||||||
|
Assert.Equal ("{ }", HxVals.From [])
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``From succeeds and escapes quotes`` () =
|
||||||
|
Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }",
|
||||||
|
HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ])
|
||||||
|
|
||||||
|
|
||||||
/// Tests for the HtmxAttrs module
|
/// Tests for the HtmxAttrs module
|
||||||
module Attributes =
|
module Attributes =
|
||||||
@@ -145,6 +344,10 @@ module Attributes =
|
|||||||
let ``_hxDisable succeeds`` () =
|
let ``_hxDisable succeeds`` () =
|
||||||
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
|
p [ _hxDisable ] [] |> shouldRender """<p hx-disable></p>"""
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``_hxDisinherit succeeds`` () =
|
||||||
|
strong [ _hxDisinherit "*" ] [] |> shouldRender """<strong hx-disinherit="*"></strong>"""
|
||||||
|
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
let ``_hxEncoding succeeds`` () =
|
let ``_hxEncoding succeeds`` () =
|
||||||
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
|
form [ _hxEncoding "utf-7" ] [] |> shouldRender """<form hx-encoding="utf-7"></form>"""
|
||||||
@@ -226,6 +429,10 @@ module Attributes =
|
|||||||
let ``_hxSwapOob succeeds`` () =
|
let ``_hxSwapOob succeeds`` () =
|
||||||
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
|
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``_hxSync succeeds`` () =
|
||||||
|
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
|
||||||
|
|
||||||
[<Fact>]
|
[<Fact>]
|
||||||
let ``_hxTarget succeeds`` () =
|
let ``_hxTarget succeeds`` () =
|
||||||
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
|
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
|
||||||
@@ -243,3 +450,18 @@ module Attributes =
|
|||||||
let ``_hxWs succeeds`` () =
|
let ``_hxWs succeeds`` () =
|
||||||
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""
|
ul [ _hxWs "connect:/web-socket" ] [] |> shouldRender """<ul hx-ws="connect:/web-socket"></ul>"""
|
||||||
|
|
||||||
|
|
||||||
|
/// Tests for the Script module
|
||||||
|
module Script =
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Script.minified succeeds`` () =
|
||||||
|
let html = RenderView.AsString.htmlNode Script.minified
|
||||||
|
Assert.Equal ("""<script src="https://unpkg.com/htmx.org@1.7.0" integrity="sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo" crossorigin="anonymous"></script>""",
|
||||||
|
html)
|
||||||
|
|
||||||
|
[<Fact>]
|
||||||
|
let ``Script.unminified succeeds`` () =
|
||||||
|
let html = RenderView.AsString.htmlNode Script.unminified
|
||||||
|
Assert.Equal ("""<script src="https://unpkg.com/htmx.org@1.7.0/dist/htmx.js" integrity="sha384-ESk4PjE7dwjGkEciohREmmf8rLMX0E95MKwxM3bvC90sZ3XbF2TELnVk2w7bX0d9" crossorigin="anonymous"></script>""",
|
||||||
|
html)
|
||||||
|
|||||||
@@ -4,10 +4,12 @@
|
|||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
|
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
|
||||||
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Include="Htmx.fs" />
|
<Compile Include="Htmx.fs" />
|
||||||
|
<None Include="README.md" Pack="true" PackagePath="\" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
module Giraffe.ViewEngine.Htmx
|
module Giraffe.ViewEngine.Htmx
|
||||||
|
|
||||||
|
/// Serialize a list of key/value pairs to JSON (very rudimentary)
|
||||||
|
let private toJson (kvps : (string * string) list) =
|
||||||
|
kvps
|
||||||
|
|> List.map (fun kvp -> sprintf "\"%s\": \"%s\"" (fst kvp) ((snd kvp).Replace ("\"", "\\\"")))
|
||||||
|
|> String.concat ", "
|
||||||
|
|> sprintf "{ %s }"
|
||||||
|
|
||||||
|
|
||||||
/// Valid values for the `hx-encoding` attribute
|
/// Valid values for the `hx-encoding` attribute
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxEncoding =
|
module HxEncoding =
|
||||||
@@ -8,7 +16,13 @@ module HxEncoding =
|
|||||||
/// A multipart form (used for file uploads)
|
/// A multipart form (used for file uploads)
|
||||||
let MultipartForm = "multipart/form-data"
|
let MultipartForm = "multipart/form-data"
|
||||||
|
|
||||||
// TODO: hx-header helper
|
|
||||||
|
/// Helper to create the `hx-headers` attribute
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxHeaders =
|
||||||
|
/// Create headers from a list of key/value pairs
|
||||||
|
let From = toJson
|
||||||
|
|
||||||
|
|
||||||
/// Values / helpers for the `hx-params` attribute
|
/// Values / helpers for the `hx-params` attribute
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
@@ -22,7 +36,25 @@ module HxParams =
|
|||||||
/// Exclude the specified parameters
|
/// Exclude the specified parameters
|
||||||
let Except fields = With fields |> sprintf "not %s"
|
let Except fields = With fields |> sprintf "not %s"
|
||||||
|
|
||||||
// TODO: hx-request helper
|
|
||||||
|
/// Helpers to define `hx-request` attribute values
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxRequest =
|
||||||
|
/// Convert a boolean to its lowercase string equivalent
|
||||||
|
let private toLowerBool (it : bool) =
|
||||||
|
(string it).ToLowerInvariant ()
|
||||||
|
/// Configure the request with various options
|
||||||
|
let Configure (opts : string list) =
|
||||||
|
opts
|
||||||
|
|> String.concat ", "
|
||||||
|
|> sprintf "{ %s }"
|
||||||
|
/// Set a timeout (in milliseconds)
|
||||||
|
let Timeout (ms : int) = $"\"timeout\": {ms}"
|
||||||
|
/// Include or exclude credentials from the request
|
||||||
|
let Credentials = toLowerBool >> sprintf "\"credentials\": %s"
|
||||||
|
/// Exclude or include headers from the request
|
||||||
|
let NoHeaders = toLowerBool >> sprintf "\"noHeaders\": %s"
|
||||||
|
|
||||||
|
|
||||||
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config)
|
/// Valid values for the `hx-swap` attribute (may be combined with swap/settle/scroll/show config)
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
@@ -42,6 +74,7 @@ module HxSwap =
|
|||||||
/// Does not append content from response (out of band items will still be processed).
|
/// Does not append content from response (out of band items will still be processed).
|
||||||
let None = "none"
|
let None = "none"
|
||||||
|
|
||||||
|
|
||||||
/// Helpers for the `hx-trigger` attribute
|
/// Helpers for the `hx-trigger` attribute
|
||||||
[<RequireQualifiedAccess>]
|
[<RequireQualifiedAccess>]
|
||||||
module HxTrigger =
|
module HxTrigger =
|
||||||
@@ -56,6 +89,10 @@ module HxTrigger =
|
|||||||
let Click = "click"
|
let Click = "click"
|
||||||
/// Trigger the event on page load
|
/// Trigger the event on page load
|
||||||
let Load = "load"
|
let Load = "load"
|
||||||
|
/// Trigger the event when the item is visible
|
||||||
|
let Revealed = "revealed"
|
||||||
|
/// Trigger this event every [timing declaration]
|
||||||
|
let Every (duration : string) = $"every {duration}"
|
||||||
/// Helpers for defining filters
|
/// Helpers for defining filters
|
||||||
module Filter =
|
module Filter =
|
||||||
/// Only trigger the event if the `ALT` key is pressed
|
/// Only trigger the event if the `ALT` key is pressed
|
||||||
@@ -72,13 +109,51 @@ module HxTrigger =
|
|||||||
let CtrlAltShift = CtrlAlt >> Shift
|
let CtrlAltShift = CtrlAlt >> Shift
|
||||||
/// Only trigger the event if `ALT+SHIFT` are pressed
|
/// Only trigger the event if `ALT+SHIFT` are pressed
|
||||||
let AltShift = Alt >> Shift
|
let AltShift = Alt >> Shift
|
||||||
|
/// Append a modifier to the current trigger
|
||||||
// TODO: more stuff for the hx-trigger helper
|
let private appendModifier modifier current =
|
||||||
|
match current with "" -> modifier | _ -> $"{current} {modifier}"
|
||||||
// TODO: hx-vals helper
|
/// Only trigger once
|
||||||
|
let Once = appendModifier "once"
|
||||||
|
/// Trigger when changed
|
||||||
|
let Changed = appendModifier "changed"
|
||||||
|
/// Delay execution; resets every time the event is seen
|
||||||
|
let Delay = sprintf "delay:%s" >> appendModifier
|
||||||
|
/// Throttle execution; ignore other events, fire when duration passes
|
||||||
|
let Throttle = sprintf "throttle:%s" >> appendModifier
|
||||||
|
/// Trigger this event from a CSS selector
|
||||||
|
let From = sprintf "from:%s" >> appendModifier
|
||||||
|
/// Trigger this event from the `document` object
|
||||||
|
let FromDocument = From "document"
|
||||||
|
/// Trigger this event from the `window` object
|
||||||
|
let FromWindow = From "window"
|
||||||
|
/// Trigger this event from the closest parent CSS selector
|
||||||
|
let FromClosest = sprintf "closest %s" >> From
|
||||||
|
/// Trigger this event from the closest child CSS selector
|
||||||
|
let FromFind = sprintf "find %s" >> From
|
||||||
|
/// Target the given CSS selector with the results of this event
|
||||||
|
let Target = sprintf "target:%s" >> appendModifier
|
||||||
|
/// Prevent any further events from occurring after this one fires
|
||||||
|
let Consume = appendModifier "consume"
|
||||||
|
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last"
|
||||||
|
let Queue = sprintf "queue:%s" >> appendModifier
|
||||||
|
/// Queue the first event, discard all others (i.e., a FIFO queue of 1)
|
||||||
|
let QueueFirst = Queue "first"
|
||||||
|
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)
|
||||||
|
let QueueLast = Queue "last"
|
||||||
|
/// Queue all events; discard none
|
||||||
|
let QueueAll = Queue "all"
|
||||||
|
/// Queue no events; discard all
|
||||||
|
let QueueNone = Queue "none"
|
||||||
|
|
||||||
|
|
||||||
/// Attributes and flags for HTMX
|
/// Helper to create the `hx-vals` attribute
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module HxVals =
|
||||||
|
/// Create values from a list of key/value pairs
|
||||||
|
let From = toJson
|
||||||
|
|
||||||
|
|
||||||
|
/// Attributes and flags for htmx
|
||||||
[<AutoOpen>]
|
[<AutoOpen>]
|
||||||
module HtmxAttrs =
|
module HtmxAttrs =
|
||||||
/// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
|
/// Progressively enhances anchors and forms to use AJAX requests (use `_hxNoBoost` to set to false)
|
||||||
@@ -89,6 +164,8 @@ module HtmxAttrs =
|
|||||||
let _hxDelete = attr "hx-delete"
|
let _hxDelete = attr "hx-delete"
|
||||||
/// Disables htmx processing for the given node and any children nodes
|
/// Disables htmx processing for the given node and any children nodes
|
||||||
let _hxDisable = flag "hx-disable"
|
let _hxDisable = flag "hx-disable"
|
||||||
|
/// Disinherit all ("*") or specific htmx attributes
|
||||||
|
let _hxDisinherit = attr "hx-disinherit"
|
||||||
/// Changes the request encoding type
|
/// Changes the request encoding type
|
||||||
let _hxEncoding = attr "hx-encoding"
|
let _hxEncoding = attr "hx-encoding"
|
||||||
/// Extensions to use for this element
|
/// Extensions to use for this element
|
||||||
@@ -129,6 +206,8 @@ module HtmxAttrs =
|
|||||||
let _hxSwap = attr "hx-swap"
|
let _hxSwap = attr "hx-swap"
|
||||||
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
|
/// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target
|
||||||
let _hxSwapOob = attr "hx-swap-oob"
|
let _hxSwapOob = attr "hx-swap-oob"
|
||||||
|
/// Synchronize events based on another element
|
||||||
|
let _hxSync = attr "hx-sync"
|
||||||
/// Specifies the target element to be swapped
|
/// Specifies the target element to be swapped
|
||||||
let _hxTarget = attr "hx-target"
|
let _hxTarget = attr "hx-target"
|
||||||
/// Specifies the event that triggers the request
|
/// Specifies the event that triggers the request
|
||||||
@@ -137,3 +216,23 @@ module HtmxAttrs =
|
|||||||
let _hxVals = attr "hx-vals"
|
let _hxVals = attr "hx-vals"
|
||||||
/// Establishes a WebSocket or sends information to one
|
/// Establishes a WebSocket or sends information to one
|
||||||
let _hxWs = attr "hx-ws"
|
let _hxWs = attr "hx-ws"
|
||||||
|
|
||||||
|
|
||||||
|
/// Script tags to pull htmx into an web page
|
||||||
|
module Script =
|
||||||
|
|
||||||
|
/// Script tag to load the minified version from unpkg.com
|
||||||
|
let minified =
|
||||||
|
script [
|
||||||
|
_src "https://unpkg.com/htmx.org@1.7.0"
|
||||||
|
_integrity "sha384-EzBXYPt0/T6gxNp0nuPtLkmRpmDBbjg6WmCUZRLXBBwYYmwAUxzlSGej0ARHX0Bo"
|
||||||
|
_crossorigin "anonymous"
|
||||||
|
] []
|
||||||
|
|
||||||
|
/// Script tag to load the unminified version from unpkg.com
|
||||||
|
let unminified =
|
||||||
|
script [
|
||||||
|
_src "https://unpkg.com/htmx.org@1.7.0/dist/htmx.js"
|
||||||
|
_integrity "sha384-ESk4PjE7dwjGkEciohREmmf8rLMX0E95MKwxM3bvC90sZ3XbF2TELnVk2w7bX0d9"
|
||||||
|
_crossorigin "anonymous"
|
||||||
|
] []
|
||||||
|
|||||||
53
src/ViewEngine.Htmx/README.md
Normal file
53
src/ViewEngine.Htmx/README.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
## Giraffe.ViewEngine.Htmx
|
||||||
|
|
||||||
|
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
|
||||||
|
|
||||||
|
**htmx version: 1.7.0**
|
||||||
|
|
||||||
|
### Setup
|
||||||
|
|
||||||
|
1. Install the package.
|
||||||
|
2. Prior to using the attribute or support modules, `open Giraffe.ViewEngine.Htmx`.
|
||||||
|
|
||||||
|
### Use
|
||||||
|
|
||||||
|
Following Giraffe View Engine's lead, there are a set of attribute functions for htmx; for many of the attributes, there are also helper modules to assist with typing the values. The example below utilizes both:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
let autoload =
|
||||||
|
div [ _hxGet "/this/data"; _hxTrigger HxTrigger.Load ] [ str "Loading..." ]
|
||||||
|
```
|
||||||
|
|
||||||
|
Support modules include:
|
||||||
|
- `HxEncoding`
|
||||||
|
- `HxHeaders`
|
||||||
|
- `HxParams`
|
||||||
|
- `HxRequest`
|
||||||
|
- `HxSwap`
|
||||||
|
- `HxTrigger`
|
||||||
|
- `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).
|
||||||
|
|
||||||
|
### Learn
|
||||||
|
|
||||||
|
htmx's attributes and these attribute functions map one-to-one. The lone exception is `_hxBoost`, which implies `true`; use `_hxNoBoost` to set it to `false`. The support modules contain named properties for known values (as illustrated with `HxTrigger.Load` above). A few of the modules are more than collections of names, though:
|
||||||
|
- `HxRequest` has a `Configure` function, which takes a list of strings; the other functions in the module allow for configuring the request.
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
HxRequest.Configure [ HxRequest.Timeout 500 ] |> _hxRequest
|
||||||
|
```
|
||||||
|
- `HxTrigger` is _(by far)_ the most complex of these modules. Most uses won't need that complexity; however, complex triggers can be defined by piping into or composing with other functions. For example, to define an event that responds to a shift-click anywhere on the document, with a delay of 3 seconds before firing:
|
||||||
|
|
||||||
|
```fsharp
|
||||||
|
HxTrigger.Click
|
||||||
|
|> HxTrigger.Filter.Shift
|
||||||
|
|> HxTrigger.FromDocument
|
||||||
|
|> HxTrigger.Delay "3s"
|
||||||
|
|> _hxTrigger
|
||||||
|
|
||||||
|
// or
|
||||||
|
|
||||||
|
(HxTrigger.Filter.Shift >> HxTrigger.FromDocument >> HxTrigger.Delay "3s") HxTrigger.Click
|
||||||
|
|> _hxTrigger
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user