5 Commits

Author SHA1 Message Date
86defea3c1 Add scripts to README
Also prep for release
2022-01-06 21:31:36 -05:00
9a9f159cab Add script functions
Also bump version to match htmx
2022-01-04 12:54:36 -05:00
9fcba06e75 Add HX-Retarget header (#2) 2021-11-26 12:49:48 -05:00
5906f3b295 Add NuGet READMEs 2021-11-11 18:40:22 -05:00
dc06b06b1f Finish view engine helpers 2021-11-11 13:33:50 -05:00
10 changed files with 412 additions and 9 deletions

View File

@@ -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
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/).

View File

@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8" standalone="no"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<VersionPrefix>0.9.1</VersionPrefix>
<PackageReleaseNotes>Initial NuGet release</PackageReleaseNotes>
<VersionPrefix>1.6.1</VersionPrefix>
<PackageReleaseNotes>Initial production-ready release</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl>

View File

@@ -250,6 +250,17 @@ module HandlerTests =
Assert.Equal ("false", dic.["HX-Refresh"].[0])
}
[<Fact>]
let ``withHxRetarget succeeds`` () =
let ctx = Substitute.For<HttpContext> ()
let dic = HeaderDictionary ()
ctx.Response.Headers.ReturnsForAnyArgs dic |> ignore
task {
let! _ = withHxRetarget "#somewhereElse" next ctx
Assert.True (dic.ContainsKey "HX-Retarget")
Assert.Equal ("#somewhereElse", dic.["HX-Retarget"].[0])
}
[<Fact>]
let ``withHxTrigger succeeds`` () =
let ctx = Substitute.For<HttpContext> ()

View File

@@ -4,10 +4,12 @@
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>htmx header extensions and helpers for Giraffe</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Htmx.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>

View File

@@ -74,6 +74,10 @@ module Handlers =
let withHxRefresh : bool -> HttpHandler =
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
let withHxTrigger : string -> HttpHandler =
setHttpHeader "HX-Trigger"

35
src/Htmx/README.md Normal file
View 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.6.1**
### 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.

View File

@@ -15,6 +15,19 @@ module Encoding =
Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm)
/// Tests for the HxHeaders module
module Headers =
[<Fact>]
let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxHeaders.From [])
[<Fact>]
let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"one two three\", \"again\": \"four \\\"five\\\" six\" }",
HxHeaders.From [ "test", "one two three"; "again", "four \"five\" six" ])
/// Tests for the HxParams module
module Params =
@@ -51,6 +64,43 @@ module Params =
Assert.Equal ("not blue,green", HxParams.Except [ "blue"; "green" ])
/// Tests for the HxRequest module
module Request =
[<Fact>]
let ``Configure succeeds with an empty list`` () =
Assert.Equal ("{ }", HxRequest.Configure [])
[<Fact>]
let ``Configure succeeds with a non-empty list`` () =
Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ])
[<Fact>]
let ``Configure succeeds with all known params configured`` () =
Assert.Equal ("{ \"timeout\": 1000, \"credentials\": false, \"noHeaders\": true }",
HxRequest.Configure [ HxRequest.Timeout 1000; HxRequest.Credentials false; HxRequest.NoHeaders true ])
[<Fact>]
let ``Timeout succeeds`` () =
Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50)
[<Fact>]
let ``Credentials succeeds when set to true`` () =
Assert.Equal ("\"credentials\": true", HxRequest.Credentials true)
[<Fact>]
let ``Credentials succeeds when set to false`` () =
Assert.Equal ("\"credentials\": false", HxRequest.Credentials false)
[<Fact>]
let ``NoHeaders succeeds when set to true`` () =
Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true)
[<Fact>]
let ``NoHeaders succeeds when set to false`` () =
Assert.Equal ("\"noHeaders\": false", HxRequest.NoHeaders false)
/// Tests for the HxSwap module
module Swap =
@@ -94,6 +144,14 @@ module Trigger =
let ``Load is correct`` () =
Assert.Equal ("load", HxTrigger.Load)
[<Fact>]
let ``Revealed is correct`` () =
Assert.Equal ("revealed", HxTrigger.Revealed)
[<Fact>]
let ``Every succeeds`` () =
Assert.Equal ("every 3s", HxTrigger.Every "3s")
[<Fact>]
let ``Filter.Alt succeeds`` () =
Assert.Equal ("click[altKey]", HxTrigger.Filter.Alt HxTrigger.Click)
@@ -122,6 +180,147 @@ module Trigger =
let ``Filter.AltShift succeeds`` () =
Assert.Equal ("click[altKey&&shiftKey]", HxTrigger.Filter.AltShift HxTrigger.Click)
[<Fact>]
let ``Once succeeds when it is the first modifier`` () =
Assert.Equal ("once", HxTrigger.Once "")
[<Fact>]
let ``Once succeeds when it is not the first modifier`` () =
Assert.Equal ("click once", HxTrigger.Once "click")
[<Fact>]
let ``Changed succeeds when it is the first modifier`` () =
Assert.Equal ("changed", HxTrigger.Changed "")
[<Fact>]
let ``Changed succeeds when it is not the first modifier`` () =
Assert.Equal ("click changed", HxTrigger.Changed "click")
[<Fact>]
let ``Delay succeeds when it is the first modifier`` () =
Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "")
[<Fact>]
let ``Delay succeeds when it is not the first modifier`` () =
Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click")
[<Fact>]
let ``Throttle succeeds when it is the first modifier`` () =
Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "")
[<Fact>]
let ``Throttle succeeds when it is not the first modifier`` () =
Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click")
[<Fact>]
let ``From succeeds when it is the first modifier`` () =
Assert.Equal ("from:.nav", HxTrigger.From ".nav" "")
[<Fact>]
let ``From succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click")
[<Fact>]
let ``FromDocument succeeds when it is the first modifier`` () =
Assert.Equal ("from:document", HxTrigger.FromDocument "")
[<Fact>]
let ``FromDocument succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:document", HxTrigger.FromDocument "click")
[<Fact>]
let ``FromWindow succeeds when it is the first modifier`` () =
Assert.Equal ("from:window", HxTrigger.FromWindow "")
[<Fact>]
let ``FromWindow succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:window", HxTrigger.FromWindow "click")
[<Fact>]
let ``FromClosest succeeds when it is the first modifier`` () =
Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "")
[<Fact>]
let ``FromClosest succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click")
[<Fact>]
let ``FromFind succeeds when it is the first modifier`` () =
Assert.Equal ("from:find li", HxTrigger.FromFind "li" "")
[<Fact>]
let ``FromFind succeeds when it is not the first modifier`` () =
Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click")
[<Fact>]
let ``Target succeeds when it is the first modifier`` () =
Assert.Equal ("target:main", HxTrigger.Target "main" "")
[<Fact>]
let ``Target succeeds when it is not the first modifier`` () =
Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click")
[<Fact>]
let ``Consume succeeds when it is the first modifier`` () =
Assert.Equal ("consume", HxTrigger.Consume "")
[<Fact>]
let ``Consume succeeds when it is not the first modifier`` () =
Assert.Equal ("click consume", HxTrigger.Consume "click")
[<Fact>]
let ``Queue succeeds when it is the first modifier`` () =
Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "")
[<Fact>]
let ``Queue succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click")
[<Fact>]
let ``QueueFirst succeeds when it is the first modifier`` () =
Assert.Equal ("queue:first", HxTrigger.QueueFirst "")
[<Fact>]
let ``QueueFirst succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click")
[<Fact>]
let ``QueueLast succeeds when it is the first modifier`` () =
Assert.Equal ("queue:last", HxTrigger.QueueLast "")
[<Fact>]
let ``QueueLast succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:last", HxTrigger.QueueLast "click")
[<Fact>]
let ``QueueAll succeeds when it is the first modifier`` () =
Assert.Equal ("queue:all", HxTrigger.QueueAll "")
[<Fact>]
let ``QueueAll succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:all", HxTrigger.QueueAll "click")
[<Fact>]
let ``QueueNone succeeds when it is the first modifier`` () =
Assert.Equal ("queue:none", HxTrigger.QueueNone "")
[<Fact>]
let ``QueueNone succeeds when it is not the first modifier`` () =
Assert.Equal ("click queue:none", HxTrigger.QueueNone "click")
/// Tests for the HxVals module
module Vals =
[<Fact>]
let ``From succeeds with an empty list`` () =
Assert.Equal ("{ }", HxVals.From [])
[<Fact>]
let ``From succeeds and escapes quotes`` () =
Assert.Equal ("{ \"test\": \"a \\\"b\\\" c\", \"2\": \"d e f\" }",
HxVals.From [ "test", "a \"b\" c"; "2", "d e f" ])
/// Tests for the HtmxAttrs module
module Attributes =

View File

@@ -4,10 +4,12 @@
<TargetFramework>net6.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Extensions to Giraffe View Engine to support htmx attributes and their values</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Htmx.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
<ItemGroup>

View File

@@ -1,5 +1,13 @@
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
[<RequireQualifiedAccess>]
module HxEncoding =
@@ -8,7 +16,13 @@ module HxEncoding =
/// A multipart form (used for file uploads)
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
[<RequireQualifiedAccess>]
@@ -22,7 +36,25 @@ module HxParams =
/// Exclude the specified parameters
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)
[<RequireQualifiedAccess>]
@@ -42,6 +74,7 @@ module HxSwap =
/// Does not append content from response (out of band items will still be processed).
let None = "none"
/// Helpers for the `hx-trigger` attribute
[<RequireQualifiedAccess>]
module HxTrigger =
@@ -53,9 +86,13 @@ module HxTrigger =
$"{parts.[0]}[{parts.[1]}&&{filter}]"
| false -> $"{trigger}[{filter}]"
/// Trigger the event on a click
let Click = "click"
let Click = "click"
/// 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
module Filter =
/// Only trigger the event if the `ALT` key is pressed
@@ -72,10 +109,48 @@ module HxTrigger =
let CtrlAltShift = CtrlAlt >> Shift
/// Only trigger the event if `ALT+SHIFT` are pressed
let AltShift = Alt >> Shift
// TODO: more stuff for the hx-trigger helper
/// Append a modifier to the current trigger
let private appendModifier modifier current =
match current with "" -> modifier | _ -> $"{current} {modifier}"
/// 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"
// TODO: hx-vals helper
/// 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
@@ -137,3 +212,23 @@ module HtmxAttrs =
let _hxVals = attr "hx-vals"
/// Establishes a WebSocket or sends information to one
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.6.1"
_integrity "sha384-tvG/2mnCFmGQzYC1Oh3qxQ7CkQ9kMzYjWZSNtrRZygHPDDqottzEJsqS4oUVodhW"
_crossorigin "anonymous"
] []
/// Script tag to load the unminified version from unpkg.com
let unminified =
script [
_src "https://unpkg.com/htmx.org@1.6.1/dist/htmx.js"
_integrity "sha384-7G9OE6gS4pBnBGH74HojjPQ8xOEGrdBeQc7JJOc58k6LG/YVfKXARd91w9715AYG"
_crossorigin "anonymous"
] []

View 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.6.1**
### 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
```