From 6677892859d3c16f7679e99d062b3ba78f40e21b Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 24 Jan 2025 16:47:57 -0500 Subject: [PATCH] Complete doc work; add HxSync module --- src/Common/Common.fs | 7 ++ src/Tests/ViewEngine.fs | 35 +++++- src/ViewEngine.Htmx/Htmx.fs | 219 ++++++++++++++++++++++++++++-------- 3 files changed, 210 insertions(+), 51 deletions(-) diff --git a/src/Common/Common.fs b/src/Common/Common.fs index 7b13f34..85b1219 100644 --- a/src/Common/Common.fs +++ b/src/Common/Common.fs @@ -25,22 +25,29 @@ let internal toLowerBool (boolValue: bool) = module HxSwap = /// The default, replace the inner HTML of the target element + [] let InnerHtml = "innerHTML" /// Replace the entire target element with the response + [] let OuterHtml = "outerHTML" /// Insert the response before the target element + [] let BeforeBegin = "beforebegin" /// Insert the response before the first child of the target element + [] let AfterBegin = "afterbegin" /// Insert the response after the last child of the target element + [] let BeforeEnd = "beforeend" /// Insert the response after the target element + [] let AfterEnd = "afterend" /// Does not append content from response (out of band items will still be processed). + [] let None = "none" diff --git a/src/Tests/ViewEngine.fs b/src/Tests/ViewEngine.fs index 779b194..0d5e249 100644 --- a/src/Tests/ViewEngine.fs +++ b/src/Tests/ViewEngine.fs @@ -497,6 +497,32 @@ let hxRequest = ] ] +/// Tests for the HxSync module +let hxSync = + testList "HxSync" [ + test "Drop is correct" { + Expect.equal HxSync.Drop "drop" "Drop is incorrect" + } + test "Abort is correct" { + Expect.equal HxSync.Abort "abort" "Abort is incorrect" + } + test "Replace is correct" { + Expect.equal HxSync.Replace "replace" "Replace is incorrect" + } + test "Queue is correct" { + Expect.equal HxSync.Queue "queue" "Queue is incorrect" + } + test "QueueFirst is correct" { + Expect.equal HxSync.QueueFirst "queue first" "QueueFirst is incorrect" + } + test "QueueLast is correct" { + Expect.equal HxSync.QueueLast "queue last" "QueueLast is incorrect" + } + test "QueueAll is correct" { + Expect.equal HxSync.QueueAll "queue all" "QueueAll is incorrect" + } + ] + /// Tests for the HxTrigger module let hxTrigger = testList "HxTrigger" [ @@ -636,6 +662,9 @@ let hxTrigger = test "succeeds when it is not the first modifier" { Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" } + test "succeeds when no type of queueing is given" { + Expect.equal (HxTrigger.Queue "" "blur") "blur queue" "Queue modifier incorrect" + } ] testList "QueueFirst" [ test "succeeds when it is the first modifier" { @@ -757,7 +786,7 @@ let attributes = hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """
""" } test "_hxPreserve succeeds" { - img [ _hxPreserve ] |> shouldRender """""" + img [ _hxPreserve ] |> shouldRender """""" } test "_hxPrompt succeeds" { strong [ _hxPrompt "Who goes there?" ] [] @@ -792,7 +821,8 @@ let attributes = li [ _hxSwapOob "true" ] [] |> shouldRender """
  • """ } test "_hxSync succeeds" { - nav [ _hxSync "closest form:abort" ] [] |> shouldRender """""" + nav [ _hxSync "closest form" HxSync.Abort ] [] + |> shouldRender """""" } test "_hxTarget succeeds" { header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """
    """ @@ -986,6 +1016,7 @@ let allTests = hxHeaders hxParams hxRequest + hxSync hxTrigger hxVals attributes diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index 5d69ca1..7a68ed0 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -6,9 +6,11 @@ module Giraffe.ViewEngine.Htmx module HxEncoding = /// A standard HTTP form + [] let Form = "application/x-www-form-urlencoded" /// A multipart form (used for file uploads) + [] let MultipartForm = "multipart/form-data" @@ -213,9 +215,11 @@ module HxHeaders = module HxParams = /// Include all parameters + [] let All = "*" /// Include no parameters + [] let None = "none" /// Include the specified parameters @@ -267,7 +271,46 @@ module HxRequest = (toLowerBool >> sprintf "\"noHeaders\": %s") exclude +/// Helpers for the hx-sync attribute +/// Documentation +[] +module HxSync = + + /// Drop (ignore) this request if a request is already in flight + /// This is the default for hx-sync + [] + let Drop = "drop" + + /// + /// Drop (ignore) this request if a request is already in flight, and if another request occurs while this one is in + /// flight, abort this request + /// + [] + let Abort = "abort" + + /// Abort any current in-flight request and replace it with this one + [] + let Replace = "replace" + + /// Place this request in an element-associated queue + [] + let Queue = "queue" + + /// Queue only the first request received while another request is in flight + [] + let QueueFirst = "queue first" + + /// Queue only the last request received while another request is in flight + [] + let QueueLast = "queue last" + + /// Queue all requests received while another request is in flight + [] + let QueueAll = "queue all" + + /// Helpers for the hx-trigger attribute +/// Documentation [] module HxTrigger = @@ -279,93 +322,160 @@ module HxTrigger = $"{parts[0]}[{parts[1]}&&{filter}]" | false -> $"{trigger}[{filter}]" - /// Trigger the event on a click + /// Trigger the event on a click + [] let Click = "click" - /// Trigger the event on page load + /// Trigger the event on page load + [] let Load = "load" - /// Trigger the event when the item is visible + /// Trigger the event when the item is visible + [] let Revealed = "revealed" - /// Trigger this event every [timing declaration] - let Every (duration : string) = $"every {duration}" + /// Trigger this event every [timing declaration] + /// The duration on which this trigger should fire (e.g., "1s", "500ms") + /// A trigger timing specification + let Every duration = + $"every %s{duration}" - /// Helpers for defining filters + /// Helpers for defining filters module Filter = - /// Only trigger the event if the `ALT` key is pressed + /// Only trigger the event if the ALT key is pressed let Alt = appendFilter "altKey" - /// Only trigger the event if the `CTRL` key is pressed + /// Only trigger the event if the CTRL key is pressed let Ctrl = appendFilter "ctrlKey" - /// Only trigger the event if the `SHIFT` key is pressed + /// Only trigger the event if the SHIFT key is pressed let Shift = appendFilter "shiftKey" - /// Only trigger the event if `CTRL+ALT` are pressed + /// Only trigger the event if CTRL and ALT are pressed let CtrlAlt = Ctrl >> Alt - /// Only trigger the event if `CTRL+SHIFT` are pressed + /// Only trigger the event if CTRL and SHIFT are pressed let CtrlShift = Ctrl >> Shift - /// Only trigger the event if `CTRL+ALT+SHIFT` are pressed + /// Only trigger the event if CTRL, ALT, and SHIFT are pressed let CtrlAltShift = CtrlAlt >> Shift - /// Only trigger the event if `ALT+SHIFT` are pressed + /// Only trigger the event if ALT and SHIFT are pressed let AltShift = Alt >> Shift /// Append a modifier to the current trigger let private appendModifier modifier current = if current = "" then modifier else $"{current} {modifier}" - /// Only trigger once - let Once = appendModifier "once" + /// Only trigger once + /// The action which should only be fired once + /// A trigger spec to fire the given action once + let Once action = + appendModifier "once" action - /// Trigger when changed - let Changed = appendModifier "changed" + /// Trigger when changed + /// The element from which the onchange event will be emitted + /// A trigger spec to fire when the given element changes + let Changed elt = + appendModifier "changed" elt - /// Delay execution; resets every time the event is seen - let Delay = sprintf "delay:%s" >> appendModifier + /// Delay execution; resets every time the event is seen + /// The duration for the delay (e.g., "1s", "500ms") + /// The action which should be fired after the given delay + /// A trigger spec to fire the given action after the specified delay + let Delay duration action = + appendModifier $"delay:%s{duration}" action - /// Throttle execution; ignore other events, fire when duration passes - let Throttle = sprintf "throttle:%s" >> appendModifier + /// Throttle execution; ignore other events, fire when duration passes + /// The duration for the throttling (e.g., "1s", "500ms") + /// The action which should be fired after the given duration + /// A trigger spec to fire the given action after the specified duration + let Throttle duration action = + appendModifier $"throttle:%s{duration}" action - /// Trigger this event from a CSS selector - let From = sprintf "from:%s" >> appendModifier + /// Trigger this event from a CSS selector + /// A CSS selector to identify elements which may fire this trigger + /// The action to be fired + /// A trigger spec to fire from the given element(s) + let From selector action = + appendModifier $"from:%s{selector}" action - /// Trigger this event from the `document` object - let FromDocument = From "document" + /// Trigger this event from the document object + /// The action to be fired + /// A trigger spec to fire from the document element + let FromDocument action = + From "document" action - /// Trigger this event from the `window` object - let FromWindow = From "window" + /// Trigger this event from the window object + /// The action to be fired + /// A trigger spec to fire from the window object + let FromWindow action = + From "window" action - /// Trigger this event from the closest parent CSS selector - let FromClosest = sprintf "closest %s" >> From + /// Trigger this event from the closest parent CSS selector + /// The CSS selector from which the action should be fired + /// The action to be fired + /// A trigger spec to fire from the closest element + let FromClosest selector action = + From $"closest %s{selector}" action - /// Trigger this event from the closest child CSS selector - let FromFind = sprintf "find %s" >> From + /// Trigger this event from the closest child CSS selector + /// The child CSS selector from which the action should be fired + /// The action to be fired + /// A trigger spec to fire from the closest child element + let FromFind selector action = + From $"find %s{selector}" action - /// Target the given CSS selector with the results of this event - let Target = sprintf "target:%s" >> appendModifier + /// Target the given CSS selector with the results of this event + /// The CSS selector to which the result of the action will be applied + /// The action to be fired + /// A trigger spec to target the given selector + let Target selector action = + appendModifier $"target:%s{selector}" action - /// Prevent any further events from occurring after this one fires - let Consume = appendModifier "consume" + /// Prevent any further events from occurring after this one fires + /// The action to be fired + /// A trigger spec to fire the given action and prevent further events + let Consume action = + appendModifier "consume" action - /// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" - let Queue = sprintf "queue:%s" >> appendModifier + /// + /// Configure queueing when events fire when others are in flight; if unspecified, the default is last + /// + /// + /// How the request should be queued (consider , , + /// , and ) + /// + /// The action to be fired + /// A trigger spec to queue the given action + let Queue how action = + let qSpec = if how = "" then "" else $":{how}" + appendModifier $"queue{qSpec}" action - /// Queue the first event, discard all others (i.e., a FIFO queue of 1) - let QueueFirst = Queue "first" + /// Queue the first event, discard all others (i.e., a FIFO queue of 1) + /// The action to be fired + /// A trigger spec to queue the given action + let QueueFirst action = + Queue "first" action - /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) - let QueueLast = Queue "last" + /// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) + /// The action to be fired + /// A trigger spec to queue the given action + let QueueLast action = + Queue "last" action - /// Queue all events; discard none - let QueueAll = Queue "all" + /// Queue all events; discard none + /// The action to be fired + /// A trigger spec to queue the given action + let QueueAll action = + Queue "all" action - /// Queue no events; discard all - let QueueNone = Queue "none" + /// Queue no events; discard all + /// The action to be fired + /// A trigger spec to queue the given action + let QueueNone action = + Queue "none" action /// Helper to create the hx-vals attribute @@ -623,8 +733,14 @@ module HtmxAttrs = let _hxSwapOob swap = attr "hx-swap-oob" swap - /// Synchronize events based on another element - let _hxSync = attr "hx-sync" + /// Synchronize events based on another element + /// A CSS selector for the element with which this one should sync + /// The request synchronization action to perform (use HxSync values) + /// A configured hx-sync attribute + /// + /// Documentation + let _hxSync selector action = + attr "hx-sync" $"%s{selector}:%s{action}" /// Specifies the target element to be swapped /// A CSS selector or relative reference (or both) to identify the target @@ -633,8 +749,13 @@ module HtmxAttrs = let _hxTarget selector = attr "hx-target" selector - /// Specifies the event that triggers the request - let _hxTrigger = attr "hx-trigger" + /// Specifies the event that triggers the request + /// The trigger specification (use HxTrigger to create) + /// A configured hx-trigger attribute + /// + /// Documentation + let _hxTrigger spec = + attr "hx-trigger" spec /// Validate an input element (uses HTML5 validation API) /// Documentation