From bb2df73175cd995532193fba12a1ae186e193f85 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Thu, 24 Nov 2022 10:50:08 -0500 Subject: [PATCH] Add _hxValidate, fragment rendering Also bump version to 1.8.4 --- src/Common/README.md | 2 +- src/Directory.Build.props | 2 +- src/Htmx/README.md | 2 +- src/ViewEngine.Htmx.Tests/Tests.fs | 138 ++++++++++++++++++++++++++++- src/ViewEngine.Htmx/Htmx.fs | 88 ++++++++++++++++-- src/ViewEngine.Htmx/README.md | 2 +- 6 files changed, 223 insertions(+), 11 deletions(-) diff --git a/src/Common/README.md b/src/Common/README.md index 6476b31..0fa5940 100644 --- a/src/Common/README.md +++ b/src/Common/README.md @@ -2,4 +2,4 @@ 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.0** +**htmx version: 1.8.4** diff --git a/src/Directory.Build.props b/src/Directory.Build.props index dba8c92..e1a670a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,7 +2,7 @@ net6.0 - 1.8.0 + 1.8.4 Support new attributes/headers in htmx 1.8.0 danieljsummers Bit Badger Solutions diff --git a/src/Htmx/README.md b/src/Htmx/README.md index 21b550e..1bf8fc8 100644 --- a/src/Htmx/README.md +++ b/src/Htmx/README.md @@ -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 diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs index c9a4a5b..653f8e2 100644 --- a/src/ViewEngine.Htmx.Tests/Tests.fs +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -434,12 +434,146 @@ module Script = let ``Script.minified succeeds`` () = let html = RenderView.AsString.htmlNode Script.minified Assert.Equal - ("""""", + ("""""", html) [] let ``Script.unminified succeeds`` () = let html = RenderView.AsString.htmlNode Script.unminified Assert.Equal - ("""""", + ("""""", html) + + +/// Tests for the RenderFragment module +module RenderFragment = + + open System.Text + + [] + let ``RenderFragment.findIdNode fails with a Text node`` () = + Assert.False (Option.isSome (RenderFragment.findIdNode "blue" (Text ""))) + + [] + let ``RenderFragment.findIdNode fails with a VoidElement without a matching ID`` () = + Assert.False (Option.isSome (RenderFragment.findIdNode "purple" (br [ _id "mauve" ]))) + + [] + 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" ] ]))) + + [] + 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) + + [] + 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) = + $"– ID {nodeId} not found –" + + /// Tests for the AsString module + module AsString = + + [] + 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 ("""ouch""", html) + + [] + let ``RenderFragment.AsString.htmlFromNodes fails when an ID is not matched`` () = + Assert.Equal (nodeNotFound "oops", RenderFragment.AsString.htmlFromNodes "oops" []) + + [] + 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 ("""

found it

""", html) + + [] + 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 ("""

ta-da

""", html) + + [] + 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 + + [] + 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 (utf8.GetBytes """boo""", bytes) + + [] + let ``RenderFragment.AsBytes.htmlFromNodes fails when an ID is not matched`` () = + Assert.Equal (utf8.GetBytes (nodeNotFound "whiff"), RenderFragment.AsBytes.htmlFromNodes "whiff" []) + + [] + let ``RenderFragment.AsBytes.htmlFromNode succeeds when ID is matched at top level`` () = + let bytes = RenderFragment.AsBytes.htmlFromNode "first" (p [ _id "first" ] [ str "!!!" ]) + Assert.Equal (utf8.GetBytes """

!!!

""", bytes) + + [] + 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 (utf8.GetBytes """

node

""", bytes) + + [] + let ``RenderFragment.AsBytes.htmlFromNode fails when an ID is not matched`` () = + Assert.Equal (utf8.GetBytes (nodeNotFound "foo"), RenderFragment.AsBytes.htmlFromNode "foo" (hr [])) + + /// Tests for the IntoStringBuilder module + module IntoStringBuilder = + + [] + 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 (""";)""", string sb) + + [] + 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) + + [] + 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 ("""

pinnacle

""", string sb) + + [] + 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 ("""

is here

""", string sb) + + [] + 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) diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index 51a948d..32aeb7b 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -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 +[] +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) = + $"– ID {nodeId} not found –" + + /// 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 + [] + 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 + [] + 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 + [] + 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 diff --git a/src/ViewEngine.Htmx/README.md b/src/ViewEngine.Htmx/README.md index b3e9ad7..49a9d58 100644 --- a/src/ViewEngine.Htmx/README.md +++ b/src/ViewEngine.Htmx/README.md @@ -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