datastar-hs
Safe HaskellSafe-Inferred
LanguageHaskell2010

Hypermedia.Datastar

Description

Datastar is a hypermedia framework: instead of building a JSON API and a JavaScript SPA, you write HTML on the server and let Datastar handle the interactivity. The browser sends requests, the server holds the connection open as a server-sent event (SSE) stream, and pushes HTML fragments, signal updates, or scripts back to the browser as things change.

This SDK provides the server-side Haskell API. It builds on WAI so it works with Warp, Scotty, Servant, Yesod, or any other WAI-compatible framework.

Minimal example

import Hypermedia.Datastar
import Network.Wai (Application, responseLBS, requestMethod, pathInfo)
import Network.Wai.Handler.Warp qualified as Warp
import Network.HTTP.Types (status404)

app :: Application
app req respond =
  case (requestMethod req, pathInfo req) of
    ("GET", ["hello"]) ->
      respond $ sseResponse $ \gen ->
        sendPatchElements gen (patchElements "<div id=\"msg\">Hello!</div>")
    _ ->
      respond $ responseLBS status404 [] "Not found"

main :: IO ()
main = Warp.run 3000 app

Module guide

Further reading

Synopsis

Types

data EventType Source #

The two SSE event types defined by the Datastar protocol.

Every event the server sends is one of these. EventPatchElements covers both DOM patching and script execution (the protocol encodes executeScript as a special case of element patching). EventPatchSignals updates the browser's reactive signal store.

Constructors

EventPatchElements

Sent as datastar-patch-elements on the wire. Used by both PatchElements and ExecuteScript.

EventPatchSignals

Sent as datastar-patch-signals on the wire. Used by PatchSignals.

Instances

Instances details
Show EventType Source # 
Instance details

Defined in Hypermedia.Datastar.Types

Eq EventType Source # 
Instance details

Defined in Hypermedia.Datastar.Types

data ElementPatchMode Source #

How the patched HTML should be applied to the DOM.

The default mode is Outer, which replaces the target element (matched by its id attribute) including the element itself. This works well with Datastar's morphing algorithm, which preserves focus, scroll position, and CSS transitions during the replacement.

Constructors

Outer

Replace the target element and its contents (the default).

Inner

Replace only the target element's children, keeping the element itself.

Remove

Remove the target element from the DOM entirely.

Replace

Replace the target element without morphing (a hard swap).

Prepend

Insert the new content as the first child of the target element.

Append

Insert the new content as the last child of the target element.

Before

Insert the new content immediately before the target element.

After

Insert the new content immediately after the target element.

data ElementNamespace Source #

The XML namespace for the patched elements.

Almost all content uses HtmlNs (the default). Use SvgNs or MathmlNs when patching inline SVG or MathML elements so that Datastar creates them in the correct namespace.

Constructors

HtmlNs

Standard HTML namespace (the default).

SvgNs

SVG namespace — use when patching <svg> content.

MathmlNs

MathML namespace — use when patching <math> content.

Patch Elements

data PatchElements Source #

Configuration for a datastar-patch-elements SSE event.

Construct values with patchElements or removeElements, then customise with record updates.

Constructors

PatchElements 

Fields

patchElements :: Text -> PatchElements Source #

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>"

removeElements :: Text -> PatchElements Source #

Remove elements from the DOM matching a CSS selector.

removeElements "#notification"
removeElements ".stale-row"

Patch Signals

data PatchSignals Source #

Configuration for a datastar-patch-signals SSE event.

Construct values with patchSignals, then customise with record updates.

Constructors

PatchSignals 

Fields

  • psSignals :: Text

    JSON object containing the signal values to patch. Uses JSON Merge Patch semantics: set a key to update it, set to null to remove it.

  • psOnlyIfMissing :: Bool

    When True, signal values are only set if the key doesn't already exist in the browser's store. Useful for setting initial state that shouldn't overwrite user changes (e.g. form input defaults). Default: False.

  • psEventId :: Maybe Text

    Optional SSE event ID for reconnection. See PatchElements for details.

  • psRetryDuration :: Int

    SSE retry interval in milliseconds. Default: 1000.

patchSignals :: Text -> PatchSignals Source #

Build a PatchSignals event with sensible defaults.

The argument is a JSON object (as Text) describing the signals to update.

patchSignals "{\"count\": 42, \"label\": \"hello\"}"

Execute Script

data ExecuteScript Source #

Configuration for executing a script in the browser.

Construct values with executeScript, then customise with record updates.

Constructors

ExecuteScript 

Fields

  • esScript :: Text

    The JavaScript code to execute.

  • esAutoRemove :: Bool

    Whether the <script> tag should remove itself from the DOM after executing. Default: True. Set to False if the script defines functions or variables that need to persist in the page.

  • esAttributes :: [Text]

    Extra attributes to add to the <script> tag. For example, ["type=\"module\""] to use ES module imports, or ["nonce=\"abc123\""] for CSP compliance.

  • esEventId :: Maybe Text

    Optional SSE event ID for reconnection.

  • esRetryDuration :: Int

    SSE retry interval in milliseconds. Default: 1000.

executeScript :: Text -> ExecuteScript Source #

Build an ExecuteScript event with sensible defaults.

The argument is the JavaScript source code to run in the browser.

executeScript "document.getElementById(\"name\").focus()"

WAI

data ServerSentEventGenerator Source #

An opaque handle for sending SSE events to the browser.

Obtain one from the callback passed to sseResponse. The handle is thread-safe — you can send events from multiple threads concurrently.

You don't construct these directly; sseResponse creates one for you.

sseResponse :: DatastarLogger -> (ServerSentEventGenerator -> IO ()) -> Response Source #

Create a WAI Response that streams SSE events.

The callback receives a ServerSentEventGenerator for sending events. The SSE connection stays open until the callback returns.

app :: WAI.Request -> (WAI.Response -> IO b) -> IO b
app req respond =
  respond $ sseResponse $ \gen -> do
    sendPatchElements gen (patchElements "<div id=\"msg\">Hello</div>")

sendPatchElements :: ServerSentEventGenerator -> PatchElements -> IO () Source #

Send a PatchElements event, morphing HTML into the browser's DOM.

sendPatchSignals :: ServerSentEventGenerator -> PatchSignals -> IO () Source #

Send a PatchSignals event, updating the browser's reactive signal store.

sendExecuteScript :: ServerSentEventGenerator -> ExecuteScript -> IO () Source #

Send an ExecuteScript event, running JavaScript in the browser.

readSignals :: FromJSON a => Request -> IO (Either String a) Source #

Decode signals sent by the browser in a Datastar request.

For GET requests, signals are URL-encoded in the datastar query parameter. For POST requests (and other methods), signals are read from the request body as JSON.

Define a Haskell data type with a FromJSON instance to decode into:

data MySignals = MySignals { count :: Int, label :: Text }
  deriving (Generic)
  deriving anyclass (FromJSON)

handler :: WAI.Request -> IO ()
handler req = do
  Right signals <- readSignals req
  putStrLn $ "Count is: " <> show (count signals)

isDatastarRequest :: Request -> Bool Source #

Check whether a request was initiated by Datastar.

Datastar adds a datastar-request header to its SSE requests. Use this to distinguish Datastar requests from normal page loads — for example, to serve either an SSE stream or a full HTML page from the same route.

app req respond
  | isDatastarRequest req =
      respond $ sseResponse $ \gen -> ...
  | otherwise =
      respond $ responseLBS status200 [] fullPageHtml

Logger