Datastar Racket SDK
1 Usage
2 Server Setup
datastar-tcp@
3 Reading Requests
read-signals
datastar-request?
4 SSE Events
4.1 SSE Generator
datastar-sse
sse?
close-sse
sse-closed?
call-with-sse-lock
with-sse-lock
4.2 Sending Events
patch-elements
patch-elements/  xexpr
remove-elements
patch-signals
execute-script
redirect
console-log
console-error
replace-url
4.3 Patch Modes
patch-mode-outer
patch-mode-inner
patch-mode-remove
patch-mode-replace
patch-mode-prepend
patch-mode-append
patch-mode-before
patch-mode-after
element-patch-mode/  c
4.4 Element Namespaces
element-namespace-html
element-namespace-svg
element-namespace-mathml
element-namespace/  c
5 Compression
6 Frontend Helpers
6.1 Attribute Helpers
data-attr
data-bind
data-class
data-computed
data-effect
data-ignore
data-ignore-morph
data-indicator
data-init
data-json-signals
data-on
data-on-intersect
data-on-interval
data-on-signal-patch
data-preserve-attrs
data-ref
data-show
data-signals
data-style
data-text
6.1.1 Pro Attributes
data-custom-validity
data-match-media
data-on-raf
data-on-resize
data-persist
data-query-string
data-replace-url
data-scroll-into-view
data-view-transition
6.2 Action Helpers
sse-get
sse-post
sse-put
sse-patch
sse-delete
6.3 CDN
datastar-cdn-url
datastar-cdn-map-url
datastar-version
7 Testing Utilities
make-mock-sse
make-recording-sse
sse-event
9.1.0.11

Datastar Racket SDK🔗ℹ

Jay Bonthius <jay@jmbmail.com>

 (require datastar) package: datastar-lib

This package provides a Racket SDK for working with Datastar.

1 Usage🔗ℹ

Example usage (adapted from the Datastar Go SDK docs)

#lang racket
 
(require datastar
         web-server/http
         web-server/safety-limits
         web-server/servlet-dispatch
         web-server/web-server
         json)
 
(struct store (message count) #:transparent)
 
(define (handler req)
  ; Read signals from the request
  (define signals-data (read-signals req))
  (define current-store
    (store (hash-ref signals-data 'message "")
           (hash-ref signals-data 'count 0)))
 
  ; Create a Server-Sent Event response
  (datastar-sse
   (lambda (sse)
     ; Patch elements in the DOM
     (patch-elements/xexpr sse '(div ((id "output")) "Hello from Datastar!"))
 
     ; Remove elements from the DOM
     (remove-elements sse "#temporary-element")
 
     ; Patch signals (update client-side state)
     (patch-signals sse
                    (hash 'message
                          "Updated message"
                          'count
                          (+ (store-count current-store) 1)))
 
     ; Execute JavaScript in the browser
     (execute-script sse "console.log(\"Hello from server!\")")
 
     ; Redirect the browser
     (redirect sse "/new-page"))))
 
(define stop
  (serve #:dispatch (dispatch/servlet handler)
         #:tcp@ datastar-tcp@
         #:listen-ip "127.0.0.1"
         #:port 8000
         #:connection-close? #t
         #:safety-limits (make-safety-limits #:response-timeout +inf.0
                                             #:response-send-timeout +inf.0)))
 
(with-handlers ([exn:break? (lambda (e) (stop))])
  (sync/enable-break never-evt))

For more advanced usage with streaming updates, use the callback to loop directly. If the client disconnects or a send fails, an exception is raised which datastar-sse catches automatically, triggering cleanup via on-close:

(define (streaming-handler req)
  (datastar-sse (lambda (sse)
                  (for ([i (in-range 10)])
                    (patch-elements/xexpr sse
                                          `(div ((id "counter"))
                                                ,(format "Count: ~a" i)))
                    (patch-signals sse (hash 'counter i))
                    (sleep 1)))))

You can also use the #:on-close callback for cleanup when the connection ends:

(define connections (mutable-set))
 
(define (streaming-handler req)
  (datastar-sse (lambda (sse)
                  (set-add! connections sse)
                  (console-log sse "connected"))
                #:on-close (lambda (sse) (set-remove! connections sse))))

For more examples, see the examples directory on GitHub.

2 Server Setup🔗ℹ

A tcp^ unit for use with serve’s #:tcp@ parameter. Enables instant client disconnect detection for SSE connections.

When provided, datastar-sse monitors the underlying TCP input port and interrupts on-open as soon as the client goes away, ensuring prompt cleanup via on-close. Without it, everything still works but disconnections are only detected on the next failed write.

Use dispatch/servlet from web-server/servlet-dispatch to convert your servlet into a dispatcher for serve:

(serve #:dispatch (dispatch/servlet handler)
       #:tcp@ datastar-tcp@
       #:connection-close? #t
       #:safety-limits (make-safety-limits #:response-timeout +inf.0
                                           #:response-send-timeout +inf.0))

dispatch/servlet composes with the standard web server dispatchers (dispatch-sequencer, dispatch-filter, dispatch-files, etc.) for routing and static file serving. See the examples directory for a full example.

3 Reading Requests🔗ℹ

procedure

(read-signals request)  jsexpr?

  request : request?
Parses incoming signal data from the browser. For GET requests, extracts data from the datastar query parameter. For other methods, parses the request body as JSON. This is a standalone function that operates on the request and does not require an SSE generator.

procedure

(datastar-request? request)  boolean?

  request : request?
Returns #t if the request has a Datastar-Request: true header, meaning it came from a Datastar action. The check is case-insensitive.

4 SSE Events🔗ℹ

Functions for creating and sending Datastar SSE events. See the Datastar SSE events reference for full details on event types and their data lines.

4.1 SSE Generator🔗ℹ

procedure

(datastar-sse on-open [#:on-close on-close])  response?

  on-open : (-> sse? any)
  on-close : (or/c (-> sse? any) #f) = #f
Creates an HTTP response with proper SSE headers. Calls on-open with a fresh sse? generator that can be used to send events to the client. When on-open returns (or raises an exception), the connection is closed and on-close is called if provided.

When the server is set up with datastar-tcp@, client disconnections are detected immediately: the SDK monitors the TCP input port and interrupts on-open as soon as the client goes away, ensuring prompt cleanup via on-close.

Important: When using serve, the following settings are required for SSE to work correctly:

  • #:tcp@ should be datastar-tcp@ for instant disconnect detection. Without this, disconnections are only detected on the next failed write, which means on-close may not fire promptly if the handler is blocked waiting for data.

  • #:connection-close? must be #t. Without this, the web server uses chunked transfer encoding with an internal pipe that silently absorbs writes to dead connections, preventing disconnect detection from working and on-close from firing.

  • #:safety-limits must disable both #:response-timeout and #:response-send-timeout (set to +inf.0). The defaults of 60 seconds will kill idle SSE connections. #:response-timeout limits the total time a handler can run, and #:response-send-timeout limits the time between successive writes. Both must be infinite for long-lived SSE connections.

procedure

(sse? v)  boolean?

  v : any/c
Returns #t if v is an SSE generator created by datastar-sse.

procedure

(close-sse sse)  void?

  sse : sse?
Explicitly closes the SSE connection. This is called automatically when the on-open callback returns, but can be called earlier if needed. Safe to call multiple times.

procedure

(sse-closed? sse)  boolean?

  sse : sse?
Returns #t if the SSE connection is closed, either because close-sse was called or because the underlying output port was closed (e.g., client disconnected). This is a non-destructive check that does not attempt to write to the connection.

procedure

(call-with-sse-lock sse thunk)  any

  sse : sse?
  thunk : (-> any)
Holds the SSE generator’s lock for the duration of thunk, preventing concurrent sending of SSE events. This ensures that multiple sends are delivered as an atomic batch without events from other threads interleaving.

This is only needed when multiple threads send through the same sse? generator. If each generator is used by a single thread (the common case), individual sends are already thread-safe and this is not needed.

The lock is re-entrant: all send functions (patch-elements, patch-signals, etc.) use call-with-sse-lock internally, so calling them inside a locked region does not deadlock.

(call-with-sse-lock sse
                    (lambda ()
                      (patch-elements/xexpr sse '(div ((id "a")) "part 1"))
                      (patch-elements/xexpr sse '(div ((id "b")) "part 2"))
                      (patch-signals sse (hash 'status "updated"))))

If an exception is raised inside thunk, the lock is released via dynamic-wind, so subsequent sends can still proceed.

syntax

(with-sse-lock sse body ...)

Syntax form that wraps body ... in a call to call-with-sse-lock.

(with-sse-lock sse
               (patch-elements/xexpr sse '(div ((id "a")) "part 1"))
               (patch-elements/xexpr sse '(div ((id "b")) "part 2"))
               (patch-signals sse (hash 'status "updated")))

4.2 Sending Events🔗ℹ

All send functions take an sse? generator as their first argument. If the connection is closed or an I/O error occurs, an exception is raised. Within datastar-sse, these exceptions are caught automatically, triggering cleanup via on-close. Sends are thread-safe: multiple threads can send events through the same generator and delivery order is serialized. If multiple threads share a single generator, use with-sse-lock to send a group of events without interleaving.

procedure

(patch-elements sse 
  elements 
  [#:selector selector 
  #:mode mode 
  #:namespace namespace 
  #:use-view-transitions use-view-transitions 
  #:event-id event-id 
  #:retry-duration retry-duration]) 
  void?
  sse : sse?
  elements : (or/c string? #f)
  selector : (or/c string? #f) = #f
  mode : element-patch-mode/c = #f
  namespace : element-namespace/c = #f
  use-view-transitions : (or/c boolean? #f) = #f
  event-id : (or/c string? #f) = #f
  retry-duration : (or/c exact-positive-integer? #f) = #f
Sends a datastar-patch-elements SSE event that patches one or more elements in the DOM. By default, Datastar morphs elements by matching top-level elements based on their ID.

The #:mode parameter controls how elements are patched. Use the named constants: patch-mode-outer (morph entire element, default), patch-mode-inner (morph inner HTML), patch-mode-replace (replace entire element), patch-mode-prepend, patch-mode-append, patch-mode-before, patch-mode-after, and patch-mode-remove. When #f or patch-mode-outer, the mode data line is omitted.

(patch-elements sse "<div id=\"out\">hello</div>")
(patch-elements sse "<svg>...</svg>" #:namespace element-namespace-svg)
(patch-elements sse "<li>item</li>" #:selector "#list" #:mode patch-mode-append)

procedure

(patch-elements/xexpr 
  sse 
  xexpr 
  [#:selector selector 
  #:mode mode 
  #:namespace namespace 
  #:use-view-transitions use-view-transitions 
  #:event-id event-id 
  #:retry-duration retry-duration]) 
  void?
  sse : sse?
  xexpr : xexpr/c
  selector : (or/c string? #f) = #f
  mode : element-patch-mode/c = #f
  namespace : element-namespace/c = #f
  use-view-transitions : (or/c boolean? #f) = #f
  event-id : (or/c string? #f) = #f
  retry-duration : (or/c exact-positive-integer? #f) = #f
Like patch-elements, but accepts an x-expression instead of a raw HTML string. Converts xexpr via xexpr->string and delegates to patch-elements.

(patch-elements/xexpr sse '(div ((id "out")) "hello"))
(patch-elements/xexpr sse '(svg "...") #:namespace element-namespace-svg)
(patch-elements/xexpr sse
                      '(li "item")
                      #:selector "#list"
                      #:mode patch-mode-append)

procedure

(remove-elements sse    
  selector    
  [#:event-id event-id    
  #:retry-duration retry-duration])  void?
  sse : sse?
  selector : string?
  event-id : (or/c string? #f) = #f
  retry-duration : (or/c exact-positive-integer? #f) = #f
Removes elements from the DOM by CSS selector. Convenience function that calls patch-elements with patch-mode-remove.

procedure

(patch-signals sse    
  signals    
  [#:event-id event-id    
  #:only-if-missing only-if-missing    
  #:retry-duration retry-duration])  void?
  sse : sse?
  signals : (or/c string? jsexpr?)
  event-id : (or/c string? #f) = #f
  only-if-missing : (or/c boolean? #f) = #f
  retry-duration : (or/c exact-positive-integer? #f) = #f
Sends a datastar-patch-signals SSE event that patches signals into the existing signals on the page. The #:only-if-missing option determines whether to update each signal only if a signal with that name does not yet exist.

procedure

(execute-script sse    
  script    
  [#:auto-remove auto-remove    
  #:attributes attributes    
  #:event-id event-id    
  #:retry-duration retry-duration])  void?
  sse : sse?
  script : string?
  auto-remove : boolean? = #t
  attributes : (or/c (hash/c symbol? any/c) (listof string?) #f)
   = #f
  event-id : (or/c string? #f) = #f
  retry-duration : (or/c exact-positive-integer? #f) = #f
Sends a datastar-execute-script SSE event that executes JavaScript in the browser by injecting a <script> element. The script element is automatically removed after execution unless auto-remove is #f.

procedure

(redirect sse location)  void?

  sse : sse?
  location : string?
Redirects the browser to a new location using window.location. This is a convenience function that calls execute-script.

procedure

(console-log sse message)  void?

  sse : sse?
  message : string?
Logs a message to the browser console via console.log. The message is automatically quoted as a JavaScript string. This is a convenience function that calls execute-script.

procedure

(console-error sse message)  void?

  sse : sse?
  message : string?
Same as console-log but uses console.error.

procedure

(replace-url sse location)  void?

  sse : sse?
  location : string?
Updates the browser URL without navigating, using window.history.replaceState. This is a convenience function that calls execute-script.

4.3 Patch Modes🔗ℹ

Named constants for the #:mode parameter of patch-elements. Values are symbols ('outer, 'inner, etc.).

Morph the entire element (default).
Replace inner HTML.
Remove the element.
Replace the element without morphing.
Prepend inside the element.
Append inside the element.
Insert before the element.
Insert after the element.

Contract for valid patch modes: any of the patch-mode-* constants or #f.

4.4 Element Namespaces🔗ℹ

Named constants for the #:namespace parameter of patch-elements. Values are symbols ('html, 'svg, 'mathml).

HTML namespace (default).
SVG namespace.
MathML namespace.

Contract for valid namespaces: any of the element-namespace-* constants or #f.

5 Compression🔗ℹ

To add Brotli compression, wrap your app handler with wrap-brotli-compress from web-server-compress:

(require datastar
         web-server-compress
         web-server/dispatch
         web-server/servlet-dispatch
         web-server/web-server)
 
(define (events-handler req)
  (datastar-sse (lambda (sse) ...)))
 
(define-values (app _uri) (dispatch-rules ...))
 
(serve #:dispatch (dispatch/servlet (wrap-brotli-compress app)) ...)

Responses are compressed only when the client sends Accept-Encoding: br. Each SSE event is flushed to the client immediately. Optional keyword arguments like #:quality and #:compress? let you tune the encoder or override which responses are compressed; see the web-server-compress docs for the full API.

6 Frontend Helpers🔗ℹ

Convenience functions for generating Datastar HTML attributes and backend action strings in x-expression templates. These are not part of the core SDK protocol; they provide Racket-friendly sugar over raw data-* attribute strings.

6.1 Attribute Helpers🔗ℹ

Functions for generating Datastar data-* HTML attributes as x-expression attribute pairs. Each function returns (list 'attr-name "value") which drops directly into x-expression templates via unquote. See the Datastar attribute reference for full details on each attribute’s behavior.

Without attribute helpers:

> `(button ((data-on:click__debounce.500ms "@post('/search')")
            (data-class:active "$enabled")
            (data-show "$query != ''")))

'(button

  ((data-on:click__debounce.500ms "@post('/search')")

   (data-class:active "$enabled")

   (data-show "$query != ''")))

With attribute helpers:

> `(button (,(data-on "click" (sse-post "/search") #:debounce "500ms")
            ,(data-class 'active "$enabled")
            ,(data-show "$query != ''")))

'(button

  ((data-on:click__debounce.500ms "@post('/search')")

   (data-class:active "$enabled")

   (data-show "$query != ''")))

Modifiers (debounce, throttle, once, etc.) are expressed as keyword arguments rather than method chaining. Boolean modifiers take #t; parameterized modifiers take their value directly.

procedure

(data-attr key-or-hash [value-or-unused])  list?

  key-or-hash : (or/c symbol? string? hash?)
  value-or-unused : any/c = #f
Generates a data-attr attribute that sets the value of any HTML attribute to an expression, and keeps it in sync.

In the keyed form, key-or-hash is the attribute name and value-or-unused is the expression.

In the hash form, key-or-hash is a hash mapping attribute names to expressions.

Keyed form:

> `(button (,(data-attr 'disabled "$loading")))

'(button ((data-attr:disabled "$loading")))

Hash form:

> `(button (,(data-attr (hash "disabled" "$loading" "aria-busy" "$loading"))))

'(button ((data-attr "{\"disabled\": $loading, \"aria-busy\": $loading}")))

procedure

(data-bind signal [value])  list?

  signal : string?
  value : string? = ""
Generates a data-bind attribute that creates a signal (if one doesn’t already exist) and sets up two-way data binding between it and an element’s value.

The signal is the signal name. The optional value is rarely needed since the element’s own value is used.

> `(input (,(data-bind "username")))

'(input ((data-bind:username "")))

> `(select (,(data-bind "choice"))
           (option ((value "a")) "A")
           (option ((value "b")) "B"))

'(select

  ((data-bind:choice ""))

  (option ((value "a")) "A")

  (option ((value "b")) "B"))

procedure

(data-class key-or-hash [value-or-unused])  list?

  key-or-hash : (or/c symbol? string? hash?)
  value-or-unused : any/c = #f
Generates a data-class attribute that adds or removes a class to or from an element based on an expression.

In the keyed form, key-or-hash is the class name and value-or-unused is a boolean expression.

In the hash form, key-or-hash is a hash mapping class names to boolean expressions.

Keyed form:

> `(button (,(data-class 'active "$selected")))

'(button ((data-class:active "$selected")))

Hash form:

> `(div (,(data-class (hash "font-bold" "$important" "text-red" "$error"))))

'(div ((data-class "{\"text-red\": $error, \"font-bold\": $important}")))

procedure

(data-computed key-or-hash [value-or-unused])

  (or/c list? (listof list?))
  key-or-hash : (or/c symbol? string? hash?)
  value-or-unused : any/c = #f
Generates a data-computed attribute that creates a signal that is computed based on an expression. The computed signal is read-only, and its value is automatically updated when any signals in the expression are updated.

In the keyed form, key-or-hash is the signal name and value-or-unused is the expression.

In the hash form, key-or-hash is a hash mapping signal names to expressions. The hash form returns a list of attribute pairs, not a single pair. Use ,@"@" (unquote-splicing) to insert them into an x-expression.

Keyed form:

> `(div (,(data-computed 'total "$price * $quantity")))

'(div ((data-computed:total "$price * $quantity")))

Hash form (returns list of pairs, use ,@"@"):

> `(div (,@(data-computed (hash 'total "$price * $qty" 'valid "$total > 0"))))

'(div

  ((data-computed:total "$price * $qty") (data-computed:valid "$total > 0")))

procedure

(data-effect expression)  list?

  expression : string?
Generates a data-effect attribute that executes expression on page load and whenever any signals in the expression change. This is useful for performing side effects.

> `(div (,(data-effect "$total = $price * $quantity")))

'(div ((data-effect "$total = $price * $quantity")))

procedure

(data-ignore [#:self self])  list?

  self : boolean? = #f
Generates a data-ignore attribute that tells Datastar to ignore this element and its descendants when walking the DOM.

When #:self is #t, only the element itself is ignored; its children are still processed.

> `(div (,(data-ignore)) "Datastar will not process this or its children")

'(div ((data-ignore "")) "Datastar will not process this or its children")

> `(div (,(data-ignore #:self #t)) "Only this element is ignored")

'(div ((data-ignore__self "")) "Only this element is ignored")

procedure

(data-ignore-morph)  list?

Generates a data-ignore-morph attribute that tells Datastar’s element patcher to skip this element and its children when morphing. Takes no arguments.

> `(div (,(data-ignore-morph)) "This content will not be morphed")

'(div ((data-ignore-morph "")) "This content will not be morphed")

procedure

(data-indicator signal)  list?

  signal : string?
Generates a data-indicator attribute that creates a signal and sets its value to true while a fetch request is in flight, otherwise false.

> `(button (,(data-indicator "loading") ,(data-on "click" (sse-get "/data"))) "Fetch")

'(button ((data-indicator:loading "") (data-on:click "@get('/data')")) "Fetch")

> `(div (,(data-show "$loading")) "Loading...")

'(div ((data-show "$loading")) "Loading...")

procedure

(data-init expression    
  [#:delay delay    
  #:viewtransition viewtransition])  list?
  expression : string?
  delay : (or/c string? number? #f) = #f
  viewtransition : boolean? = #f
Generates a data-init attribute that runs expression when the attribute is initialized. This can happen on page load, when an element is patched into the DOM, and any time the attribute is modified.

> (data-init (sse-get "/events"))

'(data-init "@get('/events')")

procedure

(data-json-signals [#:include include    
  #:exclude exclude    
  #:terse terse])  list?
  include : (or/c string? #f) = #f
  exclude : (or/c string? #f) = #f
  terse : boolean? = #f
Generates a data-json-signals attribute that sets the text content of an element to a reactive JSON stringified version of signals. Useful for troubleshooting.

> `(pre (,(data-json-signals)))

'(pre ((data-json-signals "")))

> `(pre (,(data-json-signals #:include "/^user/" #:terse #t)))

'(pre ((data-json-signals__terse "{\"include\": \"/^user/\"}")))

procedure

(data-on event    
  expression    
  [#:once once    
  #:passive passive    
  #:capture capture    
  #:window window    
  #:outside outside    
  #:prevent prevent    
  #:stop stop    
  #:trust trust    
  #:debounce debounce    
  #:debounce-leading debounce-leading    
  #:debounce-notrailing debounce-notrailing    
  #:throttle throttle    
  #:throttle-noleading throttle-noleading    
  #:throttle-trailing throttle-trailing    
  #:delay delay    
  #:viewtransition viewtransition])  list?
  event : string?
  expression : string?
  once : boolean? = #f
  passive : boolean? = #f
  capture : boolean? = #f
  window : boolean? = #f
  outside : boolean? = #f
  prevent : boolean? = #f
  stop : boolean? = #f
  trust : boolean? = #f
  debounce : (or/c string? number? #f) = #f
  debounce-leading : boolean? = #f
  debounce-notrailing : boolean? = #f
  throttle : (or/c string? number? #f) = #f
  throttle-noleading : boolean? = #f
  throttle-trailing : boolean? = #f
  delay : (or/c string? number? #f) = #f
  viewtransition : boolean? = #f
Generates a data-on attribute that attaches an event listener to an element, executing expression whenever event is triggered. Keyword arguments correspond to Datastar modifiers.

> (data-on "click" "$count++")

'(data-on:click "$count++")

> (data-on "input" (sse-post "/search") #:debounce "250ms")

'(data-on:input__debounce.250ms "@post('/search')")

> (data-on "click" (sse-get "/data") #:once #t #:prevent #t)

'(data-on:click__once__prevent "@get('/data')")

> (data-on "keydown" "$handleKey(evt)" #:window #t)

'(data-on:keydown__window "$handleKey(evt)")

procedure

(data-on-intersect expression 
  [#:once once 
  #:half half 
  #:full full 
  #:exit exit 
  #:threshold threshold 
  #:debounce debounce 
  #:debounce-leading debounce-leading 
  #:debounce-notrailing debounce-notrailing 
  #:throttle throttle 
  #:throttle-noleading throttle-noleading 
  #:throttle-trailing throttle-trailing 
  #:delay delay 
  #:viewtransition viewtransition]) 
  list?
  expression : string?
  once : boolean? = #f
  half : boolean? = #f
  full : boolean? = #f
  exit : boolean? = #f
  threshold : (or/c string? number? #f) = #f
  debounce : (or/c string? number? #f) = #f
  debounce-leading : boolean? = #f
  debounce-notrailing : boolean? = #f
  throttle : (or/c string? number? #f) = #f
  throttle-noleading : boolean? = #f
  throttle-trailing : boolean? = #f
  delay : (or/c string? number? #f) = #f
  viewtransition : boolean? = #f
Generates a data-on-intersect attribute that runs expression when the element intersects with the viewport. Keyword arguments correspond to Datastar modifiers.

> (data-on-intersect (sse-get "/load-more") #:once #t #:half #t)

'(data-on-intersect__once__half "@get('/load-more')")

procedure

(data-on-interval expression    
  [#:duration duration    
  #:duration-leading duration-leading    
  #:viewtransition viewtransition])  list?
  expression : string?
  duration : (or/c string? number? #f) = #f
  duration-leading : boolean? = #f
  viewtransition : boolean? = #f
Generates a data-on-interval attribute that runs expression at a regular interval. The interval duration defaults to one second and can be modified using #:duration. Keyword arguments correspond to Datastar modifiers.

> (data-on-interval "$count++" #:duration "2s")

'(data-on-interval__duration.2s "$count++")

> (data-on-interval (sse-get "/poll") #:duration "5s" #:duration-leading #t)

'(data-on-interval__duration.5s.leading "@get('/poll')")

procedure

(data-on-signal-patch 
  expression 
  [#:include include 
  #:exclude exclude 
  #:debounce debounce 
  #:debounce-leading debounce-leading 
  #:debounce-notrailing debounce-notrailing 
  #:throttle throttle 
  #:throttle-noleading throttle-noleading 
  #:throttle-trailing throttle-trailing 
  #:delay delay]) 
  (or/c list? (listof list?))
  expression : string?
  include : (or/c string? #f) = #f
  exclude : (or/c string? #f) = #f
  debounce : (or/c string? number? #f) = #f
  debounce-leading : boolean? = #f
  debounce-notrailing : boolean? = #f
  throttle : (or/c string? number? #f) = #f
  throttle-noleading : boolean? = #f
  throttle-trailing : boolean? = #f
  delay : (or/c string? number? #f) = #f
Generates a data-on-signal-patch attribute that runs expression whenever any signals are patched.

When #:include or #:exclude are provided, a separate data-on-signal-patch-filter attribute is generated alongside the main attribute. In this case, the function returns a list of two attribute pairs instead of a single pair. Use ,@"@" (unquote-splicing) to insert them.

No filter (returns single pair):

> `(div (,(data-on-signal-patch "console.log('patched')")))

'(div ((data-on-signal-patch "console.log('patched')")))

With filter (returns list of two pairs, use ,@"@"):

> `(div (,@(data-on-signal-patch "console.log('counter changed')"
                               #:include "/^counter$/"
                               #:debounce "300ms")))

'(div

  ((data-on-signal-patch__debounce.300ms "console.log('counter changed')")

   (data-on-signal-patch-filter "{\"include\": \"/^counter$/\"}")))

procedure

(data-preserve-attrs attrs)  list?

  attrs : (or/c string? (listof string?))
Generates a data-preserve-attr attribute that preserves the value of specified attributes when morphing DOM elements.

attrs can be a single attribute name string or a list of attribute name strings.

Preserve the open attribute on a <details> element:

> `(details ((open "")) (,(data-preserve-attrs "open")) (summary "Title") "Content")

'(details

  ((open ""))

  ((data-preserve-attr "open"))

  (summary "Title")

  "Content")

Preserve multiple attributes:

> `(details ((open "") (class "custom"))
            (,(data-preserve-attrs '("open" "class")))
            (summary "Title")
            "Content")

'(details

  ((open "") (class "custom"))

  ((data-preserve-attr "open class"))

  (summary "Title")

  "Content")

procedure

(data-ref signal)  list?

  signal : string?
Generates a data-ref attribute that creates a new signal that is a reference to the element on which the attribute is placed.

> `(div (,(data-ref "myDiv")))

'(div ((data-ref:myDiv "")))

procedure

(data-show expression)  list?

  expression : string?
Generates a data-show attribute that shows or hides an element based on whether expression evaluates to true or false.

> `(div (,(data-show "$loggedIn")) "Welcome back")

'(div ((data-show "$loggedIn")) "Welcome back")

procedure

(data-signals key-or-hash    
  [value-or-unused    
  #:ifmissing ifmissing])  list?
  key-or-hash : (or/c symbol? string? hash?)
  value-or-unused : any/c = #f
  ifmissing : boolean? = #f
Generates a data-signals attribute that patches (adds, updates or removes) one or more signals into the existing signals.

In the keyed form, key-or-hash is a symbol or string signal name and value-or-unused is the signal’s initial value as a string expression.

In the hash form, key-or-hash is a Racket hash that is serialized to JSON. Nested hashes produce nested signals.

When #:ifmissing is #t, signals are only set if they don’t already exist.

Keyed form:

> `(div (,(data-signals 'count "0")))

'(div ((data-signals:count "0")))

Keyed form with ifmissing:

> `(div (,(data-signals 'count "0" #:ifmissing #t)))

'(div ((data-signals:count__ifmissing "0")))

Hash form:

> `(div (,(data-signals (hash 'count 0 'name "hello"))))

'(div ((data-signals "{\"count\":0,\"name\":\"hello\"}")))

Nested signals:

> `(div (,(data-signals (hash 'form (hash 'name "" 'email "")))))

'(div ((data-signals "{\"form\":{\"email\":\"\",\"name\":\"\"}}")))

procedure

(data-style key-or-hash [value-or-unused])  list?

  key-or-hash : (or/c symbol? string? hash?)
  value-or-unused : any/c = #f
Generates a data-style attribute that sets the value of inline CSS styles on an element based on an expression, and keeps them in sync.

In the keyed form, key-or-hash is the CSS property name and value-or-unused is the expression.

In the hash form, key-or-hash is a hash mapping CSS property names to expressions.

Keyed form:

> `(div (,(data-style 'background-color "$dark ? 'black' : 'white'")))

'(div ((data-style:background-color "$dark ? 'black' : 'white'")))

Hash form:

> `(div
    (,(data-style
       (hash "display" "$hidden && 'none'" "color" "$error ? 'red' : 'black'"))))

'(div

  ((data-style

    "{\"color\": $error ? 'red' : 'black', \"display\": $hidden && 'none'}")))

procedure

(data-text expression)  list?

  expression : string?
Generates a data-text attribute that binds the text content of an element to expression.

> `(span (,(data-text "$count")))

'(span ((data-text "$count")))

6.1.1 Pro Attributes🔗ℹ

procedure

(data-custom-validity expression)  list?

  expression : string?
Generates a data-custom-validity attribute for custom form validation. The expression must evaluate to a string: an empty string means the input is valid; a non-empty string is used as the validation error message. This is a Datastar Pro attribute.

> `(input (,(data-bind "password")
           ,(data-custom-validity
             "$password.length < 8 ? 'Must be 8+ characters' : ''")))

'(input

  ((data-bind:password "")

   (data-custom-validity

    "$password.length < 8 ? 'Must be 8+ characters' : ''")))

procedure

(data-match-media signal query)  list?

  signal : string?
  query : string?
Generates a data-match-media attribute that sets a signal to whether a media query matches, and keeps it in sync whenever the query changes. This is a Datastar Pro attribute.

> `(div (,(data-match-media "is-dark" "'prefers-color-scheme: dark'")))

'(div ((data-match-media:is-dark "'prefers-color-scheme: dark'")))

procedure

(data-on-raf expression    
  [#:throttle throttle    
  #:throttle-noleading throttle-noleading    
  #:throttle-trailing throttle-trailing])  list?
  expression : string?
  throttle : (or/c string? number? #f) = #f
  throttle-noleading : boolean? = #f
  throttle-trailing : boolean? = #f
Generates a data-on-raf attribute that runs expression on every requestAnimationFrame callback. This is a Datastar Pro attribute.

procedure

(data-on-resize expression 
  [#:debounce debounce 
  #:debounce-leading debounce-leading 
  #:debounce-notrailing debounce-notrailing 
  #:throttle throttle 
  #:throttle-noleading throttle-noleading 
  #:throttle-trailing throttle-trailing]) 
  list?
  expression : string?
  debounce : (or/c string? number? #f) = #f
  debounce-leading : boolean? = #f
  debounce-notrailing : boolean? = #f
  throttle : (or/c string? number? #f) = #f
  throttle-noleading : boolean? = #f
  throttle-trailing : boolean? = #f
Generates a data-on-resize attribute that runs expression whenever the element’s dimensions change. This is a Datastar Pro attribute.

procedure

(data-persist [#:key key    
  #:include include    
  #:exclude exclude    
  #:session session])  list?
  key : (or/c string? #f) = #f
  include : (or/c string? #f) = #f
  exclude : (or/c string? #f) = #f
  session : boolean? = #f
Generates a data-persist attribute that persists signals in localStorage (or sessionStorage with #:session). Useful for storing signal values between page loads. This is a Datastar Pro attribute.

> `(div (,(data-persist)))

'(div ((data-persist "")))

> `(div (,(data-persist #:key "myapp" #:include "/^user\\./" #:session #t)))

'(div ((data-persist:myapp__session "{\"include\": \"/^user\\\\./\"}")))

procedure

(data-query-string [#:include include    
  #:exclude exclude    
  #:filter filter    
  #:history history])  list?
  include : (or/c string? #f) = #f
  exclude : (or/c string? #f) = #f
  filter : boolean? = #f
  history : boolean? = #f
Generates a data-query-string attribute that syncs query string parameters to signal values on page load, and syncs signal values to query string parameters on change. This is a Datastar Pro attribute.

> `(div (,(data-query-string #:filter #t #:history #t)))

'(div ((data-query-string__filter__history "")))

procedure

(data-replace-url expression)  list?

  expression : string?
Generates a data-replace-url attribute that replaces the URL in the browser without reloading the page. The value can be a relative or absolute URL. This is a Datastar Pro attribute.

> `(div (,(data-replace-url "`/page${$page}`")))

'(div ((data-replace-url "`/page${$page}`")))

procedure

(data-scroll-into-view [#:smooth smooth    
  #:instant instant    
  #:auto auto    
  #:hstart hstart    
  #:hcenter hcenter    
  #:hend hend    
  #:hnearest hnearest    
  #:vstart vstart    
  #:vcenter vcenter    
  #:vend vend    
  #:vnearest vnearest    
  #:focus focus])  list?
  smooth : boolean? = #f
  instant : boolean? = #f
  auto : boolean? = #f
  hstart : boolean? = #f
  hcenter : boolean? = #f
  hend : boolean? = #f
  hnearest : boolean? = #f
  vstart : boolean? = #f
  vcenter : boolean? = #f
  vend : boolean? = #f
  vnearest : boolean? = #f
  focus : boolean? = #f
Generates a data-scroll-into-view attribute that scrolls the element into view. Useful when updating the DOM from the backend and you want to scroll to new content. Keyword arguments correspond to Datastar modifiers for scrolling behavior, horizontal/vertical alignment, and focus. This is a Datastar Pro attribute.

> (data-scroll-into-view #:smooth #t #:vcenter #t)

'(data-scroll-into-view__smooth__vcenter "")

procedure

(data-view-transition expression)  list?

  expression : string?
Generates a data-view-transition attribute that sets the view-transition-name style attribute explicitly. This is a Datastar Pro attribute.

> `(div (,(data-view-transition "$transitionName")))

'(div ((data-view-transition "$transitionName")))

6.2 Action Helpers🔗ℹ

Convenience functions for generating Datastar backend action attribute strings.

> (define tid 42)
> `(main ((id "main") ,(data-init (sse-get "/events")))
         (form (,(data-on "submit" (sse-post "/todo/create")))
               (button (,(data-on "click"
                                (sse-post (format "/todo/delete/~a" tid))))
                       "Delete")))

'(main

  ((id "main") (data-init "@get('/events')"))

  (form

   ((data-on:submit "@post('/todo/create')"))

   (button ((data-on:click "@post('/todo/delete/42')")) "Delete")))

procedure

(sse-get url [args])  string?

  url : string?
  args : string? = #f
Returns a @get action string. When args is provided, it is included as a second argument.

> (sse-get "/events")

"@get('/events')"

> (sse-get "/events" "{includeLocal: true}")

"@get('/events', {includeLocal: true})"

procedure

(sse-post url [args])  string?

  url : string?
  args : string? = #f
Returns a @post action string.

procedure

(sse-put url [args])  string?

  url : string?
  args : string? = #f
Returns a @put action string.

procedure

(sse-patch url [args])  string?

  url : string?
  args : string? = #f
Returns a @patch action string.

procedure

(sse-delete url [args])  string?

  url : string?
  args : string? = #f
Returns a @delete action string.

6.3 CDN🔗ℹ

URL for the Datastar JavaScript bundle on the jsDelivr CDN, derived from datastar-version. Use this instead of hardcoding the CDN URL:

> `(script ((type "module") (src ,datastar-cdn-url)))

'(script

  ((type "module")

   (src

    "https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.8/bundles/datastar.js")))

URL for the source map corresponding to datastar-cdn-url.

The Datastar version string (e.g., "1.0.0-RC.8"). Used to derive datastar-cdn-url and datastar-cdn-map-url.

7 Testing Utilities🔗ℹ

 (require datastar/testing) package: datastar-lib

Mock SSE generators for testing Datastar handlers without a real HTTP connection. Require this module separately from datastar.

procedure

(make-mock-sse)  
sse? (-> string?)
Creates a mock sse? generator that works with all the normal send functions (patch-elements, patch-signals, etc.) but doesn’t touch the network. Returns two values: the generator, and a thunk that returns all the SSE text that has been sent through it so far.

> (define-values (sse get-output) (make-mock-sse))
> (patch-elements/xexpr sse '(div ((id "x")) "hi"))
> (get-output)

"event: datastar-patch-elements\ndata: elements <div id=\"x\">hi</div>\n\n"

procedure

(make-recording-sse)  
sse? (-> (listof sse-event?))
Like make-mock-sse, but instead of returning raw text, the retrieval thunk returns a list of sse-event structs, one per event sent.

The events are parsed from the same SSE text that would go over the wire, so they reflect exactly what a real client would receive.

> (define-values (sse2 get-events) (make-recording-sse))
> (patch-elements/xexpr sse2 '(div "test"))
> (patch-signals sse2 (hash 'x 1))
> (define events (get-events))
> (length events)

2

> (sse-event-type (first events))

"datastar-patch-elements"

> (sse-event-type (second events))

"datastar-patch-signals"

struct

(struct sse-event (type id retry data-lines)
    #:transparent)
  type : string?
  id : (or/c string? #f)
  retry : (or/c exact-nonnegative-integer? #f)
  data-lines : (listof string?)
A parsed SSE event. Transparent, so check-equal? works on it directly.

  • type – event type string (e.g. "datastar-patch-elements").

  • id – event ID, or #f if none was set.

  • retry – retry duration in milliseconds, or #f if the default was used (defaults are omitted from SSE output).

  • data-lines – list of data line contents without the data: prefix. For example, '("elements <div>hello</div>").