datastar-hs-0.1.0.0: src/Hypermedia/Datastar/PatchElements.hs
-- |
-- Module : Hypermedia.Datastar.PatchElements
-- Description : Send HTML fragments to patch the browser DOM
--
-- \"Patch elements\" is Datastar's primary mechanism for updating the page: the
-- server sends an HTML fragment and the browser morphs it into the DOM. The
-- morphing algorithm preserves focus, scroll position, and CSS transitions,
-- so partial page updates feel seamless.
--
-- The simplest case — send HTML and let Datastar match elements by @id@ —
-- needs only 'patchElements':
--
-- @
-- sendPatchElements gen (patchElements "\<div id=\\\"count\\\"\>42\<\/div\>")
-- @
--
-- To customise the patching behaviour, use record update syntax on the result
-- of 'patchElements':
--
-- @
-- sendPatchElements gen
-- (patchElements "\<li\>new item\<\/li\>")
-- { peSelector = Just \"#todo-list\"
-- , peMode = Append
-- }
-- @
--
-- To remove elements from the DOM, use 'removeElements' with a CSS selector:
--
-- @
-- sendPatchElements gen (removeElements \"#flash-message\")
-- @
module Hypermedia.Datastar.PatchElements where
import Data.Text (Text)
import Data.Text qualified as T
import Hypermedia.Datastar.Types
-- | Configuration for a @datastar-patch-elements@ SSE event.
--
-- Construct values with 'patchElements' or 'removeElements', then customise
-- with record updates.
data PatchElements = PatchElements
{ peElements :: Maybe Text
-- ^ The HTML fragment to patch into the DOM. 'Nothing' when removing
-- elements (see 'removeElements').
, peSelector :: Maybe Text
-- ^ CSS selector for the target element. When 'Nothing' (the default),
-- Datastar matches by the @id@ attribute of the root element in
-- 'peElements'.
, peMode :: ElementPatchMode
-- ^ How to apply the patch. Default: 'Outer' (replace the matched
-- element and its contents via morphing).
, peUseViewTransition :: Bool
-- ^ Whether to wrap the DOM update in a
-- <https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API View Transition>.
-- Default: 'False'.
, peNamespace :: ElementNamespace
-- ^ XML namespace for the patched elements. Default: 'HtmlNs'. Use
-- 'SvgNs' or 'MathmlNs' when patching inline SVG or MathML.
, peEventId :: Maybe Text
-- ^ Optional SSE event ID. The browser uses this for reconnection —
-- after a dropped connection it sends @Last-Event-ID@ so the server can
-- resume from the right point.
, peRetryDuration :: Int
-- ^ SSE retry interval in milliseconds. Default: @1000@.
}
deriving (Eq, Show)
-- | Build a 'PatchElements' event with sensible defaults.
--
-- The HTML is sent as-is and Datastar matches target elements by their @id@
-- attribute.
--
-- @
-- patchElements "\<div id=\\\"greeting\\\"\>Hello!\<\/div\>"
-- @
patchElements :: Text -> PatchElements
patchElements html =
PatchElements
{ peElements = if T.null html then Nothing else Just html
, peSelector = Nothing
, peMode = defaultPatchMode
, peUseViewTransition = defaultUseViewTransition
, peNamespace = defaultNamespace
, peEventId = Nothing
, peRetryDuration = defaultRetryDuration
}
-- | Remove elements from the DOM matching a CSS selector.
--
-- @
-- removeElements \"#notification\"
-- removeElements \".stale-row\"
-- @
removeElements :: Text -> PatchElements
removeElements sel =
PatchElements
{ peElements = Nothing
, peSelector = Just sel
, peMode = Remove
, peUseViewTransition = defaultUseViewTransition
, peNamespace = defaultNamespace
, peEventId = Nothing
, peRetryDuration = defaultRetryDuration
}
toDatastarEvent :: PatchElements -> DatastarEvent
toDatastarEvent pe =
DatastarEvent
{ eventType = EventPatchElements
, eventId = peEventId pe
, retry = peRetryDuration pe
, dataLines =
concat
[ maybe [] (\s -> ["selector " <> s]) (peSelector pe)
, [ "mode " <> patchModeToText (peMode pe)
| peMode pe /= defaultPatchMode
]
, [ "useViewTransition true"
| peUseViewTransition pe
]
, [ "namespace " <> namespaceToText (peNamespace pe)
| peNamespace pe /= defaultNamespace
]
, maybe [] (map ("elements " <>) . T.lines) (peElements pe)
]
}