/// Types and functions supporting htmx attributes in Giraffe View Engine module Giraffe.ViewEngine.Htmx /// Valid values for the hx-encoding attribute [] 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" /// The events recognized by htmx [] type HxEvent = /// Send this event to an element to abort a request | Abort /// Triggered after an AJAX request has completed processing a successful response | AfterOnLoad /// Triggered after htmx has initialized a node | AfterProcessNode /// Triggered after an AJAX request has completed | AfterRequest /// Triggered after the DOM has settled | AfterSettle /// Triggered after new content has been swapped in | AfterSwap /// Triggered before htmx disables an element or removes it from the DOM | BeforeCleanupElement /// Triggered before any response processing occurs | BeforeOnLoad /// Triggered before htmx initializes a node | BeforeProcessNode /// Triggered before an AJAX request is made | BeforeRequest /// Triggered before a swap is done, allows you to configure the swap | BeforeSwap /// Triggered just before an ajax request is sent | BeforeSend /// Triggered before the request, allows you to customize parameters, headers | ConfigRequest /// /// Triggered after a trigger occurs on an element, allows you to cancel (or delay) issuing the AJAX request /// | Confirm /// Triggered on an error during cache writing | HistoryCacheError /// Triggered on a cache miss in the history subsystem | HistoryCacheMiss /// Triggered on a unsuccessful remote retrieval | HistoryCacheMissError /// Triggered on a successful remote retrieval | HistoryCacheMissLoad /// Triggered when htmx handles a history restoration action | HistoryRestore /// Triggered before content is saved to the history cache | BeforeHistorySave /// Triggered when new content is added to the DOM | Load /// /// Triggered when an element refers to a SSE event in its trigger, but no parent SSE source has been defined /// | NoSseSourceError /// Triggered when an exception occurs during the onLoad handling in htmx | OnLoadError /// Triggered after an out of band element as been swapped in | OobAfterSwap /// Triggered before an out of band element swap is done, allows you to configure the swap | OobBeforeSwap /// Triggered when an out of band element does not have a matching ID in the current DOM | OobErrorNoTarget /// Triggered after a prompt is shown | Prompt /// Triggered after an url is pushed into history | PushedIntoHistory /// Triggered when an HTTP response error (non-200 or 300 response code) occurs | ResponseError /// Triggered when a network error prevents an HTTP request from happening | SendError /// Triggered when an error occurs with a SSE source | SseError /// Triggered when a SSE source is opened | SseOpen /// Triggered when an error occurs during the swap phase | SwapError /// Triggered when an invalid target is specified | TargetError /// Triggered when a request timeout occurs | Timeout /// Triggered before an element is validated | ValidationValidate /// Triggered when an element fails validation | ValidationFailed /// Triggered when a request is halted due to validation errors | ValidationHalted /// Triggered when an ajax request aborts | XhrAbort /// Triggered when an ajax request ends | XhrLoadEnd /// Triggered when an ajax request starts | XhrLoadStart /// Triggered periodically during an ajax request that supports progress events | XhrProgress /// The htmx event name (fst) and kebab-case name (snd, for use with hx-on) static member private Values = Map [ Abort, ("abort", "abort") AfterOnLoad, ("afterOnLoad", "after-on-load") AfterProcessNode, ("afterProcessNode", "after-process-node") AfterRequest, ("afterRequest", "after-request") AfterSettle, ("afterSettle", "after-settle") AfterSwap, ("afterSwap", "after-swap") BeforeCleanupElement, ("beforeCleanupElement", "before-cleanup-element") BeforeOnLoad, ("beforeOnLoad", "before-on-load") BeforeProcessNode, ("beforeProcessNode", "before-process-node") BeforeRequest, ("beforeRequest", "before-request") BeforeSwap, ("beforeSwap", "before-swap") BeforeSend, ("beforeSend", "before-send") ConfigRequest, ("configRequest", "config-request") Confirm, ("confirm", "confirm") HistoryCacheError, ("historyCacheError", "history-cache-error") HistoryCacheMiss, ("historyCacheMiss", "history-cache-miss") HistoryCacheMissError, ("historyCacheMissError", "history-cache-miss-error") HistoryCacheMissLoad, ("historyCacheMissLoad", "history-cache-miss-load") HistoryRestore, ("historyRestore", "history-restore") BeforeHistorySave, ("beforeHistorySave", "before-history-save") Load, ("load", "load") NoSseSourceError, ("noSSESourceError", "no-sse-source-error") OnLoadError, ("onLoadError", "on-load-error") OobAfterSwap, ("oobAfterSwap", "oob-after-swap") OobBeforeSwap, ("oobBeforeSwap", "oob-before-swap") OobErrorNoTarget, ("oobErrorNoTarget", "oob-error-no-target") Prompt, ("prompt", "prompt") PushedIntoHistory, ("pushedIntoHistory", "pushed-into-history") ResponseError, ("responseError", "response-error") SendError, ("sendError", "send-error") SseError, ("sseError", "sse-error") SseOpen, ("sseOpen", "sse-open") SwapError, ("swapError", "swap-error") TargetError, ("targetError", "target-error") Timeout, ("timeout", "timeout") ValidationValidate, ("validation:validate", "validation:validate") ValidationFailed, ("validation:failed", "validation:failed") ValidationHalted, ("validation:halted", "validation:halted") XhrAbort, ("xhr:abort", "xhr:abort") XhrLoadEnd, ("xhr:loadend", "xhr:loadend") XhrLoadStart, ("xhr:loadstart", "xhr:loadstart") XhrProgress, ("xhr:progress", "xhr:progress") ] /// The htmx event name override this.ToString() = fst HxEvent.Values[this] /// The hx-on variant of the htmx event name member this.ToHxOnString() = snd HxEvent.Values[this] /// Helper to create the hx-headers attribute [] module HxHeaders = /// Create headers from a list of key/value pairs let From = Giraffe.Htmx.Common.toJson /// Values / helpers for the hx-params attribute /// Documentation [] module HxParams = /// Include all parameters [] let All = "*" /// Include no parameters [] let None = "none" /// Include the specified parameters /// One or more fields to include in the request /// The list of fields for the hx-params attribute value let With fields = match fields with [] -> "" | _ -> fields |> List.reduce (fun acc it -> $"{acc},{it}") /// Exclude the specified parameters /// One or more fields to exclude from the request /// The list of fields for the hx-params attribute value prefixed with "not" let Except fields = With fields |> sprintf "not %s" /// Helpers to define hx-request attribute values /// Documentation [] module HxRequest = open Giraffe.Htmx.Common /// Configure the request with various options /// The options to configure /// A string with the configured options let Configure (opts: string list) = opts |> String.concat ", " |> sprintf "{ %s }" /// Set a timeout (in milliseconds) /// The milliseconds for the request timeout /// A string with the configured request timeout let Timeout (ms: int) = $"\"timeout\": {ms}" /// Include or exclude credentials from the request /// true if credentials should be sent, false if not /// A string with the configured credential options let Credentials send = (toLowerBool >> sprintf "\"credentials\": %s") send /// Exclude or include headers from the request /// /// true if no headers should be sent; false if headers should be sent /// /// A string with the configured header options let NoHeaders exclude = (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 = /// Append a filter to a trigger let private appendFilter filter (trigger : string) = match trigger.Contains "[" with | true -> let parts = trigger.Split ('[', ']') $"{parts[0]}[{parts[1]}&&{filter}]" | false -> $"{trigger}[{filter}]" /// Trigger the event on a click [] let Click = "click" /// Trigger the event on page load [] let Load = "load" /// Trigger the event when the item is visible [] let Revealed = "revealed" /// 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 module Filter = /// Only trigger the event if the ALT key is pressed let Alt = appendFilter "altKey" /// Only trigger the event if the CTRL key is pressed let Ctrl = appendFilter "ctrlKey" /// Only trigger the event if the SHIFT key is pressed let Shift = appendFilter "shiftKey" /// Only trigger the event if CTRL and ALT are pressed let CtrlAlt = Ctrl >> Alt /// Only trigger the event if CTRL and SHIFT are pressed let CtrlShift = Ctrl >> Shift /// Only trigger the event if CTRL, ALT, and SHIFT are pressed let CtrlAltShift = CtrlAlt >> Shift /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// 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 /// /// /// 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) /// 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) /// The action to be fired /// A trigger spec to queue the given action let QueueLast action = Queue "last" action /// 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 /// 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 /// Documentation [] module HxVals = /// Create values from a list of key/value pairs let From = Giraffe.Htmx.Common.toJson open Giraffe.Htmx /// Attributes and flags for htmx [] module HtmxAttrs = /// Progressively enhances anchors and forms to use AJAX requests /// Use _hxNoBoost to set to false /// Documentation let _hxBoost = attr "hx-boost" "true" /// Shows a confirm() dialog before issuing a request /// The prompt to present to the user when seeking their confirmation /// A configured hx-confirm attribute /// Documentation let _hxConfirm prompt = attr "hx-confirm" prompt /// Issues a DELETE to the specified URL /// The URL to which the DELETE request should be sent /// A configured hx-delete attribute /// Documentation let _hxDelete url = attr "hx-delete" url /// Disables htmx processing for the given node and any children nodes /// Documentation let _hxDisable = flag "hx-disable" /// Specifies elements that should be disabled when an htmx request is in flight /// The element to disable when an htmx request is in flight /// A configured hx-disabled-elt attribute /// Documentation let _hxDisabledElt elt = attr "hx-disabled-elt" elt /// Disinherit all ("*") or specific htmx attributes /// The htmx attributes to disinherit (should start with "hx-") /// A configured hx-disinherit attribute /// Documentation let _hxDisinherit hxAttrs = attr "hx-disinherit" hxAttrs /// Changes the request encoding type /// The encoding type (use HxEncoding constants) /// A configured hx-encoding attribute /// /// Documentation let _hxEncoding enc = attr "hx-encoding" enc /// Extensions to use for this element /// A list of extensions to apply to this element /// A configured hx-ext attribute /// Documentation let _hxExt exts = attr "hx-ext" exts /// Issues a GET to the specified URL /// The URL to which the GET request should be sent /// A configured hx-get attribute /// Documentation let _hxGet url = attr "hx-get" url /// Adds to the headers that will be submitted with the request /// The headers to include with the request /// A configured hx-headers attribute /// Documentation let _hxHeaders hdrs = attr "hx-headers" hdrs /// /// Set to "false" to prevent pages with sensitive information from being stored in the history cache /// /// Whether the page should be stored in the history cache /// A configured hx-history attribute /// Documentation let _hxHistory shouldStore = attr "hx-history" (toLowerBool shouldStore) /// The element to snapshot and restore during history navigation /// Documentation let _hxHistoryElt = flag "hx-history-elt" /// Includes additional data in AJAX requests /// The specification of what should be included in the request /// A configured hx-include attribute /// Documentation let _hxInclude spec = attr "hx-include" spec /// The element to put the htmx-request class on during the AJAX request /// The selector for the indicator element /// A configured hx-indicator attribute /// Documentation let _hxIndicator selector = attr "hx-indicator" selector /// Overrides a previous hx-boost (hx-boost="false") /// Documentation let _hxNoBoost = attr "hx-boost" "false" /// Attach an event handler for DOM events /// The name of the event /// The script to be executed when the event occurs /// A configured hx-on attribute /// Documentation let _hxOnEvent evtName handler = attr $"hx-on:%s{evtName}" handler /// Attach an event handler for htmx events /// The HxEvent to be handled /// The script to be executed when the event occurs /// A configured hx-on:: attribute /// /// Documentation let _hxOnHxEvent (hxEvent: HxEvent) handler = _hxOnEvent $":{hxEvent.ToHxOnString()}" handler /// Filters the parameters that will be submitted with a request /// The fields to include (use HxParams to generate this value) /// A configured hx-params attribute /// /// Documentation let _hxParams toInclude = attr "hx-params" toInclude /// Issues a PATCH to the specified URL /// The URL to which the request should be directed /// A configured hx-patch attribute /// Documentation let _hxPatch url = attr "hx-patch" url /// Issues a POST to the specified URL /// The URL to which the request should be directed /// A configured hx-post attribute /// Documentation let _hxPost url = attr "hx-post" url /// Preserves an element between requests /// Documentation let _hxPreserve = flag "hx-preserve" /// Shows a prompt() dialog before submitting a request /// The text for the prompt /// A configured hx-prompt attribute /// The value provided will be in the HX-Prompt request header /// Documentation let _hxPrompt text = attr "hx-prompt" text /// Pushes the URL into the location bar, creating a new history entry /// ///
    ///
  • "true" to push the fetched URL
  • ///
  • "false" to explicitly not push the fetched URL
  • ///
  • A specific URL to push
  • ///
/// /// A configured hx-push-url attribute /// Documentation let _hxPushUrl spec = attr "hx-push-url" spec /// Issues a PUT to the specified URL /// The URL to which the request should be directed /// A configured hx-put attribute /// Documentation let _hxPut url = attr "hx-put" url /// Replaces the current URL in the browser's history stack /// ///
    ///
  • "true" to replace the current URL with the fetched one
  • ///
  • "false" to explicitly replace nothing
  • ///
  • A specific URL to replace in the browser's history
  • ///
/// /// A configured hx-replace-url attribute /// Documentation let _hxReplaceUrl spec = attr "hx-replace-url" spec /// Configures various aspects of the request /// The configuration spec (use HxRequest.Configure to create value) /// A configured hx-request attribute /// /// Documentation let _hxRequest spec = attr "hx-request" spec /// Selects a subset of the server response to process /// A CSS selector for the content to be selected /// A configured hx-select attribute /// Documentation let _hxSelect selector = attr "hx-select" selector /// Selects a subset of an out-of-band server response /// One or more comma-delimited CSS selectors for the content to be selected /// A configured hx-select-oob attribute /// Documentation let _hxSelectOob selectors = attr "hx-select-oob" selectors /// /// Controls how the response content is swapped into the DOM (e.g. outerHTML or beforeEnd) /// /// The type of swap to perform (use HxSwap values) /// A configured hx-swap attribute /// Documentation let _hxSwap swap = attr "hx-swap" swap /// /// Controls how the response content is swapped into the DOM (e.g. outerHTML or beforeEnd), enabling /// CSS transitions /// /// The type of swap to perform (use HxSwap values) /// A configured hx-swap attribute /// Documentation let _hxSwapWithTransition swap = _hxSwap $"%s{swap} transition:true" /// /// Marks content in a response as being "Out of Band", i.e. swapped somewhere other than the target /// /// ///
    ///
  • "true" to mark this as an OOB swap
  • ///
  • Any HxSwap value
  • ///
  • Any HxSwap value, followed by a colon (:) and a CSS selector
  • ///
/// /// A configured hx-swap-oob attribute /// Documentation let _hxSwapOob swap = attr "hx-swap-oob" swap /// 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 /// A configured hx-target attribute /// Documentation let _hxTarget selector = attr "hx-target" selector /// 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 let _hxValidate = flag "hx-validate" /// Adds to the parameters that will be submitted with the request /// The values for the parameters (use HxVals.From to create) /// A configured hx-vals attribute /// /// Documentation let _hxVals values = attr "hx-vals" values /// The URL of the SSE server /// The URL from which events will be received /// A configured sse-connect attribute /// Extension Docs let _sseConnect url = attr "sse-connect" url /// The name(s) of the message(s) to swap into the DOM /// The message names (comma-delimited) to swap (use "message" for unnamed events) /// A configured sse-swap attribute /// Extension Docs let _sseSwap messages = attr "sse-swap" messages /// Script tags to pull htmx into a web page module Script = /// Script tag to load the minified version from unpkg.com let minified = script [ _src "https://unpkg.com/htmx.org@2.0.4" _integrity "sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+" _crossorigin "anonymous" ] [] /// Script tag to load the unminified version from unpkg.com let unminified = script [ _src "https://unpkg.com/htmx.org@2.0.4/dist/htmx.js" _integrity "sha384-oeUn82QNXPuVkGCkcrInrS1twIxKhkZiFfr2TdiuObZ3n3yIeMiqcRzkIcguaof1" _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 /// The id attribute to find /// The node tree to search /// The node with the requested id attribute, or None if it was not found 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 (findIdNode nodeId) /// Functions to render a fragment as a string [] module AsString = /// Render to HTML for the given ID /// The id attribute for the node to be rendered /// The node trees to search /// The HTML for the given id node, or an error message if it was not found let htmlFromNodes nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.AsString.htmlNode idNode | None -> nodeNotFound nodeId /// Render to HTML for the given ID /// The id attribute for the node to be rendered /// The node tree to search /// The HTML for the given id node, or an error message if it was not found 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 bytes for the given ID /// The id attribute for the node to be rendered /// The node trees to search /// The bytes for the given id node, or an error message if it was not found let htmlFromNodes nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.AsBytes.htmlNode idNode | None -> nodeNotFound nodeId |> utf8.GetBytes /// Render to bytes for the given ID /// The id attribute for the node to be rendered /// The node tree to search /// The bytes for the given id node, or an error message if it was not found 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 HTML into a StringBuilder for the given ID /// The StringBuilder into which the bytes will be rendered /// The id attribute for the node to be rendered /// The node trees to search let htmlFromNodes sb nodeId (nodes: XmlNode list) = match nodes |> List.tryPick (findIdNode nodeId) with | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode | None -> nodeNotFound nodeId |> sb.Append |> ignore /// Render HTML into a StringBuilder for the given ID /// The StringBuilder into which the bytes will be rendered /// The id attribute for the node to be rendered /// The node tree to search let htmlFromNode sb nodeId node = match findIdNode nodeId node with | Some idNode -> RenderView.IntoStringBuilder.htmlNode sb idNode | None -> nodeNotFound nodeId |> sb.Append |> ignore