3 Commits

Author SHA1 Message Date
061f6e5a4e Add link to frag render post 2022-11-24 11:34:07 -05:00
bb2df73175 Add _hxValidate, fragment rendering
Also bump version to 1.8.4
2022-11-24 10:50:08 -05:00
e0c567098d Add files to package common project 2022-07-14 11:32:42 -04:00
7 changed files with 233 additions and 11 deletions

View File

@@ -2,10 +2,13 @@
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Description>Common definitions for Giraffe.Htmx</Description>
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="Common.fs" />
<None Include="README.md" Pack="true" PackagePath="\" />
</ItemGroup>
</Project>

5
src/Common/README.md Normal file
View File

@@ -0,0 +1,5 @@
## Giraffe.Htmx.Common
This package contains common code shared between [`Giraffe.Htmx`](https://www.nuget.org/packages/Giraffe.Htmx) and [`Giraffe.ViewEngine.Htmx`](https://www.nuget.org/packages/Giraffe.ViewEngine.Htmx), and will be automatically installed when you install either one.
**htmx version: 1.8.4**

View File

@@ -2,8 +2,8 @@
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<VersionPrefix>1.8.0</VersionPrefix>
<PackageReleaseNotes>Support new attributes/headers in htmx 1.8.0</PackageReleaseNotes>
<VersionPrefix>1.8.4</VersionPrefix>
<PackageReleaseNotes>Support new hx-validate attribute in htmx 1.8.1; add support for fragment rendering</PackageReleaseNotes>
<Authors>danieljsummers</Authors>
<Company>Bit Badger Solutions</Company>
<PackageProjectUrl>https://github.com/bit-badger/Giraffe.Htmx</PackageProjectUrl>

View File

@@ -2,7 +2,7 @@
This package enables server-side support for [htmx](https://htmx.org) within [Giraffe](https://giraffe.wiki) and ASP.NET's `HttpContext`.
**htmx version: 1.8.0**
**htmx version: 1.8.4**
### Setup

View File

@@ -434,12 +434,146 @@ module Script =
let ``Script.minified succeeds`` () =
let html = RenderView.AsString.htmlNode Script.minified
Assert.Equal
("""<script src="https://unpkg.com/htmx.org@1.8.0" integrity="sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc" crossorigin="anonymous"></script>""",
("""<script src="https://unpkg.com/htmx.org@1.8.4" integrity="sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV" 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.8.0/dist/htmx.js" integrity="sha384-mrsv860ohrJ5KkqRxwXXj6OIT6sONUxOd+1kvbqW351hQd7JlfFnM0tLetA76GU0" crossorigin="anonymous"></script>""",
("""<script src="https://unpkg.com/htmx.org@1.8.4/dist/htmx.js" integrity="sha384-sh63gh7zpjxu153RyKJ06Oy5HxIVl6cchze/dJOHulOI7u0sGZoC/CfQJHPODhFn" crossorigin="anonymous"></script>""",
html)
/// Tests for the RenderFragment module
module RenderFragment =
open System.Text
[<Fact>]
let ``RenderFragment.findIdNode fails with a Text node`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "blue" (Text "")))
[<Fact>]
let ``RenderFragment.findIdNode fails with a VoidElement without a matching ID`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "purple" (br [ _id "mauve" ])))
[<Fact>]
let ``RenderFragment.findIdNode fails with a ParentNode with no children with a matching ID`` () =
Assert.False (Option.isSome (RenderFragment.findIdNode "green" (p [] [ str "howdy"; span [] [ str "huh" ] ])))
[<Fact>]
let ``RenderFragment.findIdNode succeeds with a VoidElement with a matching ID`` () =
let leNode = hr [ _id "groovy" ]
let foundNode = RenderFragment.findIdNode "groovy" leNode
Assert.True (Option.isSome foundNode)
Assert.Same (leNode, foundNode.Value)
[<Fact>]
let ``RenderFragment.findIdNode succeeds with a ParentNode with a child with a matching ID`` () =
let leNode = span [ _id "its-me" ] [ str "Mario" ]
let foundNode = RenderFragment.findIdNode "its-me" (p [] [ str "test"; str "again"; leNode; str "un mas" ])
Assert.True (Option.isSome foundNode)
Assert.Same (leNode, foundNode.Value)
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// Tests for the AsString module
module AsString =
[<Fact>]
let ``RenderFragment.AsString.htmlFromNodes succeeds when an ID is matched`` () =
let html =
RenderFragment.AsString.htmlFromNodes "needle"
[ p [] []; p [ _id "haystack" ] [ span [ _id "needle" ] [ str "ouch" ]; str "hay"; str "hay" ]]
Assert.Equal ("""<span id="needle">ouch</span>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNodes fails when an ID is not matched`` () =
Assert.Equal (nodeNotFound "oops", RenderFragment.AsString.htmlFromNodes "oops" [])
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode succeeds when ID is matched at top level`` () =
let html = RenderFragment.AsString.htmlFromNode "wow" (p [ _id "wow" ] [ str "found it" ])
Assert.Equal ("""<p id="wow">found it</p>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode succeeds when ID is matched in child element`` () =
let html =
div [] [ p [] [ str "not it" ]; p [ _id "hey" ] [ str "ta-da" ]]
|> RenderFragment.AsString.htmlFromNode "hey"
Assert.Equal ("""<p id="hey">ta-da</p>""", html)
[<Fact>]
let ``RenderFragment.AsString.htmlFromNode fails when an ID is not matched`` () =
Assert.Equal (nodeNotFound "me", RenderFragment.AsString.htmlFromNode "me" (hr []))
/// Tests for the AsBytes module
module AsBytes =
/// Alias for UTF-8 encoding
let private utf8 = Encoding.UTF8
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNodes succeeds when an ID is matched`` () =
let bytes =
RenderFragment.AsBytes.htmlFromNodes "found"
[ p [] []; p [ _id "not-it" ] [ str "nope"; span [ _id "found" ] [ str "boo" ]; str "nope" ]]
Assert.Equal<byte> (utf8.GetBytes """<span id="found">boo</span>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNodes fails when an ID is not matched`` () =
Assert.Equal<byte> (utf8.GetBytes (nodeNotFound "whiff"), RenderFragment.AsBytes.htmlFromNodes "whiff" [])
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode succeeds when ID is matched at top level`` () =
let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ])
Assert.Equal<byte> (utf8.GetBytes """<p id="first">!!!</p>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode succeeds when ID is matched in child element`` () =
let bytes =
div [] [ p [] [ str "not me" ]; p [ _id "child" ] [ str "node" ]]
|> RenderFragment.AsBytes.htmlFromNode "child"
Assert.Equal<byte> (utf8.GetBytes """<p id="child">node</p>""", bytes)
[<Fact>]
let ``RenderFragment.AsBytes.htmlFromNode fails when an ID is not matched`` () =
Assert.Equal<byte> (utf8.GetBytes (nodeNotFound "foo"), RenderFragment.AsBytes.htmlFromNode "foo" (hr []))
/// Tests for the IntoStringBuilder module
module IntoStringBuilder =
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNodes succeeds when an ID is matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "find-me"
[ p [] []; p [ _id "peekaboo" ] [ str "bzz"; str "nope"; span [ _id "find-me" ] [ str ";)" ] ]]
Assert.Equal ("""<span id="find-me">;)</span>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNodes fails when an ID is not matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNodes sb "missing" []
Assert.Equal (nodeNotFound "missing", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode succeeds when ID is matched at top level`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "top" (p [ _id "top" ] [ str "pinnacle" ])
Assert.Equal ("""<p id="top">pinnacle</p>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode succeeds when ID is matched in child element`` () =
let sb = StringBuilder ()
div [] [ p [] [ str "nada" ]; p [ _id "it" ] [ str "is here" ]]
|> RenderFragment.IntoStringBuilder.htmlFromNode sb "it"
Assert.Equal ("""<p id="it">is here</p>""", string sb)
[<Fact>]
let ``RenderFragment.IntoStringBuilder.htmlFromNode fails when an ID is not matched`` () =
let sb = StringBuilder ()
RenderFragment.IntoStringBuilder.htmlFromNode sb "bar" (hr [])
Assert.Equal (nodeNotFound "bar", string sb)

View File

@@ -1,4 +1,4 @@
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) =
@@ -271,6 +271,9 @@ module HtmxAttrs =
/// Specifies the event that triggers the request
let _hxTrigger = attr "hx-trigger"
/// Validate an input element (uses HTML5 validation API)
let _hxValidate = flag "hx-validate"
/// Adds to the parameters that will be submitted with the request
let _hxVals = attr "hx-vals"
@@ -283,12 +286,87 @@ module Script =
/// Script tag to load the minified version from unpkg.com
let minified =
script [ _src "https://unpkg.com/htmx.org@1.8.0"
_integrity "sha384-cZuAZ+ZbwkNRnrKi05G/fjBX+azI9DNOkNYysZ0I/X5ZFgsmMiBXgDZof30F5ofc"
script [ _src "https://unpkg.com/htmx.org@1.8.4"
_integrity "sha384-wg5Y/JwF7VxGk4zLsJEcAojRtlVp1FKKdGy1qN+OMtdq72WRvX/EdRdqg/LOhYeV"
_crossorigin "anonymous" ] []
/// Script tag to load the unminified version from unpkg.com
let unminified =
script [ _src "https://unpkg.com/htmx.org@1.8.0/dist/htmx.js"
_integrity "sha384-mrsv860ohrJ5KkqRxwXXj6OIT6sONUxOd+1kvbqW351hQd7JlfFnM0tLetA76GU0"
script [ _src "https://unpkg.com/htmx.org@1.8.4/dist/htmx.js"
_integrity "sha384-sh63gh7zpjxu153RyKJ06Oy5HxIVl6cchze/dJOHulOI7u0sGZoC/CfQJHPODhFn"
_crossorigin "anonymous" ] []
/// Functions to extract and render an HTML fragment from a document
[<RequireQualifiedAccess>]
module RenderFragment =
/// Does this element have an ID matching the requested ID name?
let private isIdElement nodeId (elt : XmlElement) =
snd elt
|> Array.exists (fun attr ->
match attr with
| KeyValue (name, value) -> name = "id" && value = nodeId
| Boolean _ -> false)
/// Generate a message if the requested ID node is not found
let private nodeNotFound (nodeId : string) =
$"<em>&ndash; ID {nodeId} not found &ndash;</em>"
/// Find the node with the named ID
let rec findIdNode nodeId (node : XmlNode) : XmlNode option =
match node with
| Text _ -> None
| VoidElement elt -> if isIdElement nodeId elt then Some node else None
| ParentNode (elt, children) ->
if isIdElement nodeId elt then Some node else children |> List.tryPick (fun c -> findIdNode nodeId c)
/// Functions to render a fragment as a string
[<RequireQualifiedAccess>]
module AsString =
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsString.htmlNode idNode
| None -> nodeNotFound nodeId
/// Functions to render a fragment as bytes
[<RequireQualifiedAccess>]
module AsBytes =
let private utf8 = System.Text.Encoding.UTF8
/// Render to HTML for the given ID
let htmlFromNodes nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Render to HTML for the given ID
let htmlFromNode nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.AsBytes.htmlNode idNode
| None -> nodeNotFound nodeId |> utf8.GetBytes
/// Functions to render a fragment into a StringBuilder
[<RequireQualifiedAccess>]
module IntoStringBuilder =
/// Render to HTML for the given ID
let htmlFromNodes sb nodeId (nodes : XmlNode list) =
match nodes |> List.tryPick(fun node -> findIdNode nodeId node) with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore
/// Render to HTML for the given ID
let htmlFromNode sb nodeId node =
match findIdNode nodeId node with
| Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode
| None -> nodeNotFound nodeId |> sb.Append |> ignore

View File

@@ -2,7 +2,7 @@
This package enables [htmx](https://htmx.org) support within the [Giraffe](https://giraffe.wiki) view engine.
**htmx version: 1.8.0**
**htmx version: 1.8.4**
### Setup
@@ -29,6 +29,8 @@ Support modules include:
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).
This also supports [fragment rendering](https://bitbadger.solutions/blog/2022/fragment-rendering-in-giraffe-view-engine.html), providing the flexibility to render an entire template, or only a portion of it (based on the element's `id` attribute).
### Learn
htmx's attributes and these attribute functions map one-to-one. 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: