Complete doc work; add HxSync module

This commit is contained in:
Daniel J. Summers 2025-01-24 16:47:57 -05:00
parent cede486099
commit 6677892859
3 changed files with 210 additions and 51 deletions

View File

@ -25,22 +25,29 @@ let internal toLowerBool (boolValue: bool) =
module HxSwap = module HxSwap =
/// <summary>The default, replace the inner HTML of the target element</summary> /// <summary>The default, replace the inner HTML of the target element</summary>
[<Literal>]
let InnerHtml = "innerHTML" let InnerHtml = "innerHTML"
/// <summary>Replace the entire target element with the response</summary> /// <summary>Replace the entire target element with the response</summary>
[<Literal>]
let OuterHtml = "outerHTML" let OuterHtml = "outerHTML"
/// <summary>Insert the response before the target element</summary> /// <summary>Insert the response before the target element</summary>
[<Literal>]
let BeforeBegin = "beforebegin" let BeforeBegin = "beforebegin"
/// <summary>Insert the response before the first child of the target element</summary> /// <summary>Insert the response before the first child of the target element</summary>
[<Literal>]
let AfterBegin = "afterbegin" let AfterBegin = "afterbegin"
/// <summary>Insert the response after the last child of the target element</summary> /// <summary>Insert the response after the last child of the target element</summary>
[<Literal>]
let BeforeEnd = "beforeend" let BeforeEnd = "beforeend"
/// <summary>Insert the response after the target element</summary> /// <summary>Insert the response after the target element</summary>
[<Literal>]
let AfterEnd = "afterend" let AfterEnd = "afterend"
/// <summary>Does not append content from response (out of band items will still be processed).</summary> /// <summary>Does not append content from response (out of band items will still be processed).</summary>
[<Literal>]
let None = "none" let None = "none"

View File

@ -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 /// Tests for the HxTrigger module
let hxTrigger = let hxTrigger =
testList "HxTrigger" [ testList "HxTrigger" [
@ -636,6 +662,9 @@ let hxTrigger =
test "succeeds when it is not the first modifier" { test "succeeds when it is not the first modifier" {
Expect.equal (HxTrigger.Queue "def" "click") "click queue:def" "Queue modifier incorrect" 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" [ testList "QueueFirst" [
test "succeeds when it is the first modifier" { test "succeeds when it is the first modifier" {
@ -757,7 +786,7 @@ let attributes =
hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">""" hr [ _hxPost "/hear-ye-hear-ye" ] |> shouldRender """<hr hx-post="/hear-ye-hear-ye">"""
} }
test "_hxPreserve succeeds" { test "_hxPreserve succeeds" {
img [ _hxPreserve ] |> shouldRender """<img hx-preserve="true">""" img [ _hxPreserve ] |> shouldRender """<img hx-preserve>"""
} }
test "_hxPrompt succeeds" { test "_hxPrompt succeeds" {
strong [ _hxPrompt "Who goes there?" ] [] strong [ _hxPrompt "Who goes there?" ] []
@ -792,7 +821,8 @@ let attributes =
li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>""" li [ _hxSwapOob "true" ] [] |> shouldRender """<li hx-swap-oob="true"></li>"""
} }
test "_hxSync succeeds" { test "_hxSync succeeds" {
nav [ _hxSync "closest form:abort" ] [] |> shouldRender """<nav hx-sync="closest form:abort"></nav>""" nav [ _hxSync "closest form" HxSync.Abort ] []
|> shouldRender """<nav hx-sync="closest form:abort"></nav>"""
} }
test "_hxTarget succeeds" { test "_hxTarget succeeds" {
header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>""" header [ _hxTarget "#somewhereElse" ] [] |> shouldRender """<header hx-target="#somewhereElse"></header>"""
@ -986,6 +1016,7 @@ let allTests =
hxHeaders hxHeaders
hxParams hxParams
hxRequest hxRequest
hxSync
hxTrigger hxTrigger
hxVals hxVals
attributes attributes

View File

@ -6,9 +6,11 @@ module Giraffe.ViewEngine.Htmx
module HxEncoding = module HxEncoding =
/// <summary>A standard HTTP form</summary> /// <summary>A standard HTTP form</summary>
[<Literal>]
let Form = "application/x-www-form-urlencoded" let Form = "application/x-www-form-urlencoded"
/// <summary>A multipart form (used for file uploads)</summary> /// <summary>A multipart form (used for file uploads)</summary>
[<Literal>]
let MultipartForm = "multipart/form-data" let MultipartForm = "multipart/form-data"
@ -213,9 +215,11 @@ module HxHeaders =
module HxParams = module HxParams =
/// <summary>Include all parameters</summary> /// <summary>Include all parameters</summary>
[<Literal>]
let All = "*" let All = "*"
/// <summary>Include no parameters</summary> /// <summary>Include no parameters</summary>
[<Literal>]
let None = "none" let None = "none"
/// <summary>Include the specified parameters</summary> /// <summary>Include the specified parameters</summary>
@ -267,7 +271,46 @@ module HxRequest =
(toLowerBool >> sprintf "\"noHeaders\": %s") exclude (toLowerBool >> sprintf "\"noHeaders\": %s") exclude
/// <summary>Helpers for the <c>hx-sync</c> attribute</summary>
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
[<RequireQualifiedAccess>]
module HxSync =
/// <summary>Drop (ignore) this request if a request is already in flight</summary>
/// <remarks>This is the default for <c>hx-sync</c></remarks>
[<Literal>]
let Drop = "drop"
/// <summary>
/// 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
/// </summary>
[<Literal>]
let Abort = "abort"
/// <summary>Abort any current in-flight request and replace it with this one</summary>
[<Literal>]
let Replace = "replace"
/// <summary>Place this request in an element-associated queue</summary>
[<Literal>]
let Queue = "queue"
/// <summary>Queue only the first request received while another request is in flight</summary>
[<Literal>]
let QueueFirst = "queue first"
/// <summary>Queue only the last request received while another request is in flight</summary>
[<Literal>]
let QueueLast = "queue last"
/// <summary>Queue all requests received while another request is in flight</summary>
[<Literal>]
let QueueAll = "queue all"
/// <summary>Helpers for the <c>hx-trigger</c> attribute</summary> /// <summary>Helpers for the <c>hx-trigger</c> attribute</summary>
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
[<RequireQualifiedAccess>] [<RequireQualifiedAccess>]
module HxTrigger = module HxTrigger =
@ -279,93 +322,160 @@ module HxTrigger =
$"{parts[0]}[{parts[1]}&&{filter}]" $"{parts[0]}[{parts[1]}&&{filter}]"
| false -> $"{trigger}[{filter}]" | false -> $"{trigger}[{filter}]"
/// Trigger the event on a click /// <summary>Trigger the event on a click</summary>
[<Literal>]
let Click = "click" let Click = "click"
/// Trigger the event on page load /// <summary>Trigger the event on page load</summary>
[<Literal>]
let Load = "load" let Load = "load"
/// Trigger the event when the item is visible /// <summary>Trigger the event when the item is visible</summary>
[<Literal>]
let Revealed = "revealed" let Revealed = "revealed"
/// Trigger this event every [timing declaration] /// <summary>Trigger this event every [timing declaration]</summary>
let Every (duration : string) = $"every {duration}" /// <param name="duration">The duration on which this trigger should fire (e.g., "1s", "500ms")</param>
/// <returns>A trigger timing specification</returns>
let Every duration =
$"every %s{duration}"
/// Helpers for defining filters /// <summary>Helpers for defining filters</summary>
module Filter = module Filter =
/// Only trigger the event if the `ALT` key is pressed /// <summary>Only trigger the event if the <c>ALT</c> key is pressed</summary>
let Alt = appendFilter "altKey" let Alt = appendFilter "altKey"
/// Only trigger the event if the `CTRL` key is pressed /// <summary>Only trigger the event if the <c>CTRL</c> key is pressed</summary>
let Ctrl = appendFilter "ctrlKey" let Ctrl = appendFilter "ctrlKey"
/// Only trigger the event if the `SHIFT` key is pressed /// <summary>Only trigger the event if the <c>SHIFT</c> key is pressed</summary>
let Shift = appendFilter "shiftKey" let Shift = appendFilter "shiftKey"
/// Only trigger the event if `CTRL+ALT` are pressed /// <summary>Only trigger the event if <c>CTRL</c> and <c>ALT</c> are pressed</summary>
let CtrlAlt = Ctrl >> Alt let CtrlAlt = Ctrl >> Alt
/// Only trigger the event if `CTRL+SHIFT` are pressed /// <summary>Only trigger the event if <c>CTRL</c> and <c>SHIFT</c> are pressed</summary>
let CtrlShift = Ctrl >> Shift let CtrlShift = Ctrl >> Shift
/// Only trigger the event if `CTRL+ALT+SHIFT` are pressed /// <summary>Only trigger the event if <c>CTRL</c>, <c>ALT</c>, and <c>SHIFT</c> are pressed</summary>
let CtrlAltShift = CtrlAlt >> Shift let CtrlAltShift = CtrlAlt >> Shift
/// Only trigger the event if `ALT+SHIFT` are pressed /// <summary>Only trigger the event if <c>ALT</c> and <c>SHIFT</c> are pressed</summary>
let AltShift = Alt >> Shift let AltShift = Alt >> Shift
/// Append a modifier to the current trigger /// Append a modifier to the current trigger
let private appendModifier modifier current = let private appendModifier modifier current =
if current = "" then modifier else $"{current} {modifier}" if current = "" then modifier else $"{current} {modifier}"
/// Only trigger once /// <summary>Only trigger once</summary>
let Once = appendModifier "once" /// <param name="action">The action which should only be fired once</param>
/// <returns>A trigger spec to fire the given action once</returns>
let Once action =
appendModifier "once" action
/// Trigger when changed /// <summary>Trigger when changed</summary>
let Changed = appendModifier "changed" /// <param name="elt">The element from which the <c>onchange</c> event will be emitted</param>
/// <returns>A trigger spec to fire when the given element changes</returns>
let Changed elt =
appendModifier "changed" elt
/// Delay execution; resets every time the event is seen /// <summary>Delay execution; resets every time the event is seen</summary>
let Delay = sprintf "delay:%s" >> appendModifier /// <param name="duration">The duration for the delay (e.g., "1s", "500ms")</param>
/// <param name="action">The action which should be fired after the given delay</param>
/// <returns>A trigger spec to fire the given action after the specified delay</returns>
let Delay duration action =
appendModifier $"delay:%s{duration}" action
/// Throttle execution; ignore other events, fire when duration passes /// <summary>Throttle execution; ignore other events, fire when duration passes</summary>
let Throttle = sprintf "throttle:%s" >> appendModifier /// <param name="duration">The duration for the throttling (e.g., "1s", "500ms")</param>
/// <param name="action">The action which should be fired after the given duration</param>
/// <returns>A trigger spec to fire the given action after the specified duration</returns>
let Throttle duration action =
appendModifier $"throttle:%s{duration}" action
/// Trigger this event from a CSS selector /// <summary>Trigger this event from a CSS selector</summary>
let From = sprintf "from:%s" >> appendModifier /// <param name="selector">A CSS selector to identify elements which may fire this trigger</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the given element(s)</returns>
let From selector action =
appendModifier $"from:%s{selector}" action
/// Trigger this event from the `document` object /// <summary>Trigger this event from the <c>document</c> object</summary>
let FromDocument = From "document" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the <c>document</c> element</returns>
let FromDocument action =
From "document" action
/// Trigger this event from the `window` object /// <summary>Trigger this event from the <c>window</c> object</summary>
let FromWindow = From "window" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the <c>window</c> object</returns>
let FromWindow action =
From "window" action
/// Trigger this event from the closest parent CSS selector /// <summary>Trigger this event from the closest parent CSS selector</summary>
let FromClosest = sprintf "closest %s" >> From /// <param name="selector">The CSS selector from which the action should be fired</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest element</returns>
let FromClosest selector action =
From $"closest %s{selector}" action
/// Trigger this event from the closest child CSS selector /// <summary>Trigger this event from the closest child CSS selector</summary>
let FromFind = sprintf "find %s" >> From /// <param name="selector">The child CSS selector from which the action should be fired</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire from the closest child element</returns>
let FromFind selector action =
From $"find %s{selector}" action
/// Target the given CSS selector with the results of this event /// <summary>Target the given CSS selector with the results of this event</summary>
let Target = sprintf "target:%s" >> appendModifier /// <param name="selector">The CSS selector to which the result of the action will be applied</param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to target the given selector</returns>
let Target selector action =
appendModifier $"target:%s{selector}" action
/// Prevent any further events from occurring after this one fires /// <summary>Prevent any further events from occurring after this one fires</summary>
let Consume = appendModifier "consume" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to fire the given action and prevent further events</returns>
let Consume action =
appendModifier "consume" action
/// Configure queueing when events fire when others are in flight; if unspecified, the default is "last" /// <summary>
let Queue = sprintf "queue:%s" >> appendModifier /// Configure queueing when events fire when others are in flight; if unspecified, the default is <c>last</c>
/// </summary>
/// <param name="how">
/// How the request should be queued (consider <see cref="QueueFirst" />, <see cref="QueueLast" />,
/// <see cref="QueueAll" />, and <see cref="QueueNone" />)
/// </param>
/// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
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) /// <summary>Queue the first event, discard all others (i.e., a FIFO queue of 1)</summary>
let QueueFirst = Queue "first" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueFirst action =
Queue "first" action
/// Queue the last event; discards current when another is received (i.e., a LIFO queue of 1) /// <summary>Queue the last event; discards current when another is received (i.e., a LIFO queue of 1)</summary>
let QueueLast = Queue "last" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueLast action =
Queue "last" action
/// Queue all events; discard none /// <summary>Queue all events; discard none</summary>
let QueueAll = Queue "all" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueAll action =
Queue "all" action
/// Queue no events; discard all /// <summary>Queue no events; discard all</summary>
let QueueNone = Queue "none" /// <param name="action">The action to be fired</param>
/// <returns>A trigger spec to queue the given action</returns>
let QueueNone action =
Queue "none" action
/// <summary>Helper to create the <c>hx-vals</c> attribute</summary> /// <summary>Helper to create the <c>hx-vals</c> attribute</summary>
@ -623,8 +733,14 @@ module HtmxAttrs =
let _hxSwapOob swap = let _hxSwapOob swap =
attr "hx-swap-oob" swap attr "hx-swap-oob" swap
/// Synchronize events based on another element /// <summary>Synchronize events based on another element</summary>
let _hxSync = attr "hx-sync" /// <param name="selector">A CSS selector for the element with which this one should sync</param>
/// <param name="action">The request synchronization action to perform (use <c>HxSync</c> values)</param>
/// <returns>A configured <c>hx-sync</c> attribute</returns>
/// <seealso cref="HxSync" />
/// <seealso href="https://htmx.org/attributes/hx-sync/">Documentation</seealso>
let _hxSync selector action =
attr "hx-sync" $"%s{selector}:%s{action}"
/// <summary>Specifies the target element to be swapped</summary> /// <summary>Specifies the target element to be swapped</summary>
/// <param name="selector">A CSS selector or relative reference (or both) to identify the target</param> /// <param name="selector">A CSS selector or relative reference (or both) to identify the target</param>
@ -633,8 +749,13 @@ module HtmxAttrs =
let _hxTarget selector = let _hxTarget selector =
attr "hx-target" selector attr "hx-target" selector
/// Specifies the event that triggers the request /// <summary>Specifies the event that triggers the request</summary>
let _hxTrigger = attr "hx-trigger" /// <param name="spec">The trigger specification (use <c>HxTrigger</c> to create)</param>
/// <returns>A configured <c>hx-trigger</c> attribute</returns>
/// <seealso cref="HxTrigger" />
/// <seealso href="https://htmx.org/attributes/hx-trigger/">Documentation</seealso>
let _hxTrigger spec =
attr "hx-trigger" spec
/// <summary>Validate an input element (uses HTML5 validation API)</summary> /// <summary>Validate an input element (uses HTML5 validation API)</summary>
/// <seealso href="https://htmx.org/attributes/hx-validate/">Documentation</seealso> /// <seealso href="https://htmx.org/attributes/hx-validate/">Documentation</seealso>