diff --git a/src/ViewEngine.Htmx.Tests/Tests.fs b/src/ViewEngine.Htmx.Tests/Tests.fs index 129562e..4e94258 100644 --- a/src/ViewEngine.Htmx.Tests/Tests.fs +++ b/src/ViewEngine.Htmx.Tests/Tests.fs @@ -15,6 +15,19 @@ module Encoding = Assert.Equal ("multipart/form-data", HxEncoding.MultipartForm) +/// Tests for the HxHeaders module +module Headers = + + [] + let ``From succeeds with an empty list`` () = + Assert.Equal ("{ }", HxHeaders.From []) + + [] + 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 = + + [] + let ``Configure succeeds with an empty list`` () = + Assert.Equal ("{ }", HxRequest.Configure []) + + [] + let ``Configure succeeds with a non-empty list`` () = + Assert.Equal ("{ \"a\": \"b\", \"c\": \"d\" }", HxRequest.Configure [ "\"a\": \"b\""; "\"c\": \"d\"" ]) + + [] + 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 ]) + + [] + let ``Timeout succeeds`` () = + Assert.Equal ("\"timeout\": 50", HxRequest.Timeout 50) + + [] + let ``Credentials succeeds when set to true`` () = + Assert.Equal ("\"credentials\": true", HxRequest.Credentials true) + + [] + let ``Credentials succeeds when set to false`` () = + Assert.Equal ("\"credentials\": false", HxRequest.Credentials false) + + [] + let ``NoHeaders succeeds when set to true`` () = + Assert.Equal ("\"noHeaders\": true", HxRequest.NoHeaders true) + + [] + 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) + [] + let ``Revealed is correct`` () = + Assert.Equal ("revealed", HxTrigger.Revealed) + + [] + let ``Every succeeds`` () = + Assert.Equal ("every 3s", HxTrigger.Every "3s") + [] 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) + [] + let ``Once succeeds when it is the first modifier`` () = + Assert.Equal ("once", HxTrigger.Once "") + + [] + let ``Once succeeds when it is not the first modifier`` () = + Assert.Equal ("click once", HxTrigger.Once "click") + + [] + let ``Changed succeeds when it is the first modifier`` () = + Assert.Equal ("changed", HxTrigger.Changed "") + + [] + let ``Changed succeeds when it is not the first modifier`` () = + Assert.Equal ("click changed", HxTrigger.Changed "click") + + [] + let ``Delay succeeds when it is the first modifier`` () = + Assert.Equal ("delay:1s", HxTrigger.Delay "1s" "") + + [] + let ``Delay succeeds when it is not the first modifier`` () = + Assert.Equal ("click delay:2s", HxTrigger.Delay "2s" "click") + + [] + let ``Throttle succeeds when it is the first modifier`` () = + Assert.Equal ("throttle:4s", HxTrigger.Throttle "4s" "") + + [] + let ``Throttle succeeds when it is not the first modifier`` () = + Assert.Equal ("click throttle:7s", HxTrigger.Throttle "7s" "click") + + [] + let ``From succeeds when it is the first modifier`` () = + Assert.Equal ("from:.nav", HxTrigger.From ".nav" "") + + [] + let ``From succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:#somewhere", HxTrigger.From "#somewhere" "click") + + [] + let ``FromDocument succeeds when it is the first modifier`` () = + Assert.Equal ("from:document", HxTrigger.FromDocument "") + + [] + let ``FromDocument succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:document", HxTrigger.FromDocument "click") + + [] + let ``FromWindow succeeds when it is the first modifier`` () = + Assert.Equal ("from:window", HxTrigger.FromWindow "") + + [] + let ``FromWindow succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:window", HxTrigger.FromWindow "click") + + [] + let ``FromClosest succeeds when it is the first modifier`` () = + Assert.Equal ("from:closest div", HxTrigger.FromClosest "div" "") + + [] + let ``FromClosest succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:closest p", HxTrigger.FromClosest "p" "click") + + [] + let ``FromFind succeeds when it is the first modifier`` () = + Assert.Equal ("from:find li", HxTrigger.FromFind "li" "") + + [] + let ``FromFind succeeds when it is not the first modifier`` () = + Assert.Equal ("click from:find .spot", HxTrigger.FromFind ".spot" "click") + + [] + let ``Target succeeds when it is the first modifier`` () = + Assert.Equal ("target:main", HxTrigger.Target "main" "") + + [] + let ``Target succeeds when it is not the first modifier`` () = + Assert.Equal ("click target:footer", HxTrigger.Target "footer" "click") + + [] + let ``Consume succeeds when it is the first modifier`` () = + Assert.Equal ("consume", HxTrigger.Consume "") + + [] + let ``Consume succeeds when it is not the first modifier`` () = + Assert.Equal ("click consume", HxTrigger.Consume "click") + + [] + let ``Queue succeeds when it is the first modifier`` () = + Assert.Equal ("queue:abc", HxTrigger.Queue "abc" "") + + [] + let ``Queue succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:def", HxTrigger.Queue "def" "click") + + [] + let ``QueueFirst succeeds when it is the first modifier`` () = + Assert.Equal ("queue:first", HxTrigger.QueueFirst "") + + [] + let ``QueueFirst succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:first", HxTrigger.QueueFirst "click") + + [] + let ``QueueLast succeeds when it is the first modifier`` () = + Assert.Equal ("queue:last", HxTrigger.QueueLast "") + + [] + let ``QueueLast succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:last", HxTrigger.QueueLast "click") + + [] + let ``QueueAll succeeds when it is the first modifier`` () = + Assert.Equal ("queue:all", HxTrigger.QueueAll "") + + [] + let ``QueueAll succeeds when it is not the first modifier`` () = + Assert.Equal ("click queue:all", HxTrigger.QueueAll "click") + + [] + let ``QueueNone succeeds when it is the first modifier`` () = + Assert.Equal ("queue:none", HxTrigger.QueueNone "") + + [] + 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 = + + [] + let ``From succeeds with an empty list`` () = + Assert.Equal ("{ }", HxVals.From []) + + [] + 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 = diff --git a/src/ViewEngine.Htmx/Htmx.fs b/src/ViewEngine.Htmx/Htmx.fs index c2ed2ee..077b8b1 100644 --- a/src/ViewEngine.Htmx/Htmx.fs +++ b/src/ViewEngine.Htmx/Htmx.fs @@ -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 [] 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 +[] +module HxHeaders = + /// Create headers from a list of key/value pairs + let From = toJson + /// Values / helpers for the `hx-params` attribute [] @@ -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 +[] +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) [] @@ -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 [] 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 +[] +module HxVals = + /// Create values from a list of key/value pairs + let From = toJson /// Attributes and flags for HTMX