API reference

Every exported helper, type, and macro. The 100-plus HTML tag constructors (div, h1, form, …) are generated programmatically and have no individual docstrings, so they aren't listed here — they come into scope with using HyperSignal (except the Base-shadowed div/select/summary/mark/time, which need @using_tags); see the Quickstart under Module overview below. Docstrings live alongside the source — this page just indexes them so the rendered docs site has clickable cross-references.

Module overview

HyperSignalModule
HyperSignal

Datastar-flavored HTML for Julia. Compose pages from typed AST nodes, render to streamed HTML, and bind Datastar actions without ever hand-typing data-on:click="@post('/x', {…})" or escaping HTML by hand.

Quickstart

using HyperSignal
HyperSignal.@using_tags                 # brings div / select / summary

# Build with tag constructors. Children go positional, attributes go kw.
page = Frag(
    DOCTYPE,
    html(lang="en",
        head(meta(charset="UTF-8"), title("My App")),
        body(
            h1("Hello"),
            form(on_submit(ds_post("/save"; form=true)),
                radio_field("size", "S", "Small"),
                radio_field("size", "L", "Large"; checked=true),
                button(type="submit", "Save")),
        )),
)

html_response(page)                   # full-page Response
fragment_response(page, "#card")      # Datastar morph with selector header

What's exported

Safety model

Strings and numbers in children / attribute values are auto-escaped at render time. Use Raw at the boundary to inject pre-built HTML (SVG snippets, audited generators) — never wrap user input in Raw. JS-string interpolation inside Datastar actions is the renderer's job; build a DSAction and let it through.

source

Element tree

HyperSignal.ElementType
Element(tag::Symbol, attrs::Vector{Pair{Symbol,Any}}, children::Vector{Any})

The HTML AST node. You almost never build one directly — call a tag constructor (div, h1, form, …) and let it split positional args / kwargs / Attribute values for you. Construct manually only if you're building a custom element with a non-static tag name.

Examples

# Direct construction — for a component with a tag chosen at runtime:
heading(level::Int, text) = Element(Symbol("h", level), Pair{Symbol,Any}[], Any[text])
heading(2, "Hello")                                          # ≡ h2("Hello")

Boolean-attribute policy

true renders the attribute as bare; false, nothing, and missing omit it entirely; any other value renders as a quoted, escaped string. Prior art (JuliaWeb/Hyperscript.jl#20) shows this corner is easy to get wrong — the doctest below pins it.

julia> render(input(type="checkbox", checked=true))
"<input type=\"checkbox\" checked>"

julia> render(input(type="checkbox", checked=false))
"<input type=\"checkbox\">"

julia> render(input(type="checkbox", checked=nothing))
"<input type=\"checkbox\">"

julia> render(input(type="checkbox", checked=missing))
"<input type=\"checkbox\">"

julia> render(input(type="checkbox", checked="yes"))
"<input type=\"checkbox\" checked=\"yes\">"
source
HyperSignal.FragType
Frag(children...)

A "group of children with no wrapper tag". Use it to return multiple sibling elements from a single function, or to prepend DOCTYPE to an html(...) tree.

Examples

# A component that renders to two siblings — without an extra wrapper div:
section_with_grid(label, cards...) = Frag(
    small(class="muted form-section-label", label),
    div(class="form-card-grid", cards...),
)
source
HyperSignal.RawType
Raw(html::String)

Trusted HTML that bypasses auto-escape. Wrap the value at the boundary — SVG icon strings, output of an audited HTML generator, third-party widget markup — never wrap user input.

Examples

const SPINNER = Raw("<svg viewBox="0 0 24 24">…</svg>")

div(class="loading", SPINNER, " Working…")  # SVG kept verbatim, " Working…" escaped

Adversarial round-trip

Raw performs zero rewriting — what you put in is what the renderer writes out. A regression that re-escapes Raw (or worse, sanitizes it) would break SVG icons and audited generators and fail this doctest. Never wrap user input.

julia> render(p(Raw("<img src=x onerror=alert(1)>")))
"<p><img src=x onerror=alert(1)></p>"
source
HyperSignal.AttributeType
Attribute(key::Symbol, value)

Attribute value returned by helpers like on and ds_indicator. Tag constructors filter these out of positional args and merge them into the attrs list, so an Attribute-returning helper drops in next to children without a splat ceremony.

You rarely construct Attribute directly — use the helpers. It's exported mostly so user code can pattern-match or filter on it.

Examples

button("Submit",
    ds_indicator(),                          # Attribute
    on(:click, ds_post("/api/submit")),      # Attribute
    "  ", strong("now"))                     # children
source
HyperSignal.DOCTYPEConstant
DOCTYPE

The <!DOCTYPE html> prelude as a Raw constant. Drop it as the first child of a Frag wrapping html(...) so page builders don't hand-type the doctype string at every call site.

Examples

page = Frag(
    DOCTYPE,
    html(lang="en",
        head(meta(charset="UTF-8"), title("My App")),
        body(h1("Hello"))),
)
html_response(page)
source

Rendering

HyperSignal.renderFunction
render(x) -> String

Render x (any renderable: Element, Frag, Raw, strings, numbers, vectors, nothing/missing) into a String. Sibling of the streaming render(io, x) — same dispatch, just returns the bytes instead of writing them. Use this when you need a String for an HTTP body or a test assertion; reach for render(io, x) when you already hold an IO.

Examples

julia> using HyperSignal: div

julia> render(div(class="card", h2("Hi"), p("hello")))
"<div class=\"card\"><h2>Hi</h2><p>hello</p></div>"

julia> render("a < b & \"c\"")
"a &lt; b &amp; &quot;c&quot;"

julia> render(nothing)
""
source

HTTP response wrappers

HyperSignal.html_responseFunction
html_response(body; status=200, headers=[]) -> HTTP.Response

Render body (anything renderable) and wrap it in an HTTP.Response with Content-Type: text/html; charset=utf-8. Use this for full-page GETs.

Examples

julia> r = html_response(p("ok"));

julia> r.status
200

julia> Dict(r.headers)["Content-Type"]
"text/html; charset=utf-8"

julia> String(r.body)
"<p>ok</p>"

julia> r2 = html_response(p("created"); status=201, headers=["X-Tag" => "v1"]);

julia> r2.status, Dict(r2.headers)["X-Tag"]
(201, "v1")
source
HyperSignal.fragment_responseFunction
fragment_response(body; selector=nothing, mode=nothing,
                  view_transition=false, status=200, headers=[]) -> HTTP.Response
fragment_response(body, selector::AbstractString; kwargs...) -> HTTP.Response

Like html_response but also surfaces the Datastar fragment control headers — datastar-selector (morph target), datastar-mode (swap mode), and datastar-use-view-transition — so a single helper covers any handler that swaps a fragment of an existing page.

  • selector — CSS selector for the morph target. Omit for whole-body morph.
  • mode::Union{Nothing,Symbol} — one of :outer :inner :replace :prepend :append :before :after :remove. nothing (the default) omits the header so the Datastar client uses its default (outer). An unknown symbol throws ArgumentError.
  • view_transition::Bool — when true, adds datastar-use-view-transition: true so the client wraps the swap in a View Transition.

The positional fragment_response(body, selector) form is preserved.

Examples

julia> r = fragment_response(p("ok"), "#count");

julia> r.status, Dict(r.headers)["datastar-selector"]
(200, "#count")

julia> String(r.body)
"<p>ok</p>"

julia> r2 = fragment_response(p("ok"); selector="#count", mode=:inner,
                              view_transition=true);

julia> Dict(r2.headers)["datastar-mode"], Dict(r2.headers)["datastar-use-view-transition"]
("inner", "true")
source
HyperSignal.signals_responseFunction
signals_response(signals; only_if_missing=false, status=200, headers=[]) -> HTTP.Response

Send a Datastar JSON-signals patch. Body is JSON.json(signals) — pass anything JSON.jl knows how to encode (NamedTuple, Dict, struct). only_if_missing=true adds the datastar-only-if-missing: true header, which tells the client to skip the merge for any signal already on the page.

Examples

julia> r = signals_response((; count=3));

julia> r.status, Dict(r.headers)["Content-Type"]
(200, "application/json; charset=utf-8")

julia> String(r.body)
"{\"count\":3}"
source
HyperSignal.script_responseFunction
script_response(js::AbstractString; script_attributes=nothing,
                status=200, headers=[]) -> HTTP.Response

Send a Datastar text/javascript response — the client appends a <script> tag with js as its body and runs it. The body is written verbatim; the caller owns the escape. Never interpolate unsanitized user input.

script_attributes becomes the datastar-script-attributes header: an AbstractString passes through; anything else is JSON-encoded with JSON.json.

Examples

julia> r = script_response("alert('hi')");

julia> r.status, Dict(r.headers)["Content-Type"]
(200, "text/javascript; charset=utf-8")

julia> String(r.body)
"alert('hi')"
source
HyperSignal.sse_responseFunction
sse_response(events; status=200, headers=[]) -> HTTP.Response

Buffer one or more Datastar SSE events (built by patch_elements / patch_signals) into a single text/event-stream response. Use this when a handler must emit an HTML patch and a signal patch in one shot.

Examples

julia> r = sse_response([
           patch_elements(HyperSignal.div(id="card", "Saved"); selector="#card"),
           patch_signals((; saved=true)),
       ]);

julia> r.status, Dict(r.headers)["Content-Type"]
(200, "text/event-stream; charset=utf-8")

julia> print(String(r.body))
event: datastar-patch-elements
data: selector #card
data: elements <div id="card">Saved</div>

event: datastar-patch-signals
data: signals {"saved":true}
source
HyperSignal.sse_streamFunction
sse_stream(f; status=200, headers=[]) -> stream handler

Build an HTTP.jl stream handler that streams Datastar SSE events over a chunked text/event-stream response. f receives a writer callable: each call with a patch_elements / patch_signals event encodes the event and flushes it as its own chunk so the client sees progress in real time. Register the returned handler with HTTP.serve(handler, host, port; stream=true).

writer is not concurrency-safe — concurrent calls from multiple tasks will interleave chunks. Serialize calls (or guard writer with a ReentrantLock) if f fans out work.

Example

HTTP.serve(sse_stream() do writer
    for i in 1:5
        writer(patch_elements(div(id="progress", "step $i"); selector="#progress", mode=:inner))
        sleep(0.5)
    end
end, "127.0.0.1", 8080; stream=true)
source
HyperSignal.patch_elementsFunction
patch_elements(body; selector=nothing, mode=nothing, view_transition=false)

Build a datastar-patch-elements SSE event for sse_response. body is rendered with render; multi-line HTML is split into one data: elements … line per source line. mode is one of the fragment modes accepted by fragment_response; unknown symbols throw ArgumentError.

source
HyperSignal.patch_signalsFunction
patch_signals(signals; only_if_missing=false)

Build a datastar-patch-signals SSE event. signals is JSON-encoded with JSON.json. only_if_missing=true adds the onlyIfMissing true data line so the client skips signals already present.

source
HyperSignal.redirect_via_fragmentFunction
redirect_via_fragment(selector, location; cookies=String[], wrapper_tag=:div) -> HTTP.Response

Datastar can't issue an HTTP 303 from a form submit it owns — the morph algorithm replaces the target instead. This helper wraps a tiny <script>window.location='…'</script> in the morph target so a Datastar form can navigate after success. Single quotes, backslashes, and </ sequences in location are escaped (the last to keep the HTML parser from closing the surrounding <script> tag mid-string).

Pass cookies as a vector of complete Set-Cookie header values to attach session cookies to the redirect — useful for the post-login flow where you need to set the cookie AND navigate in the same response. Use wrapper_tag when the morph target is something other than a <div> (e.g. :li for a <li> morph target).

For non-Datastar redirects (login form POST, plain navigation), use redirect_to instead.

Examples

# Login flow: morph #login-form to a navigation script + set session cookie
return redirect_via_fragment("#login-form", "/dashboard";
    cookies=["sid=$token; HttpOnly; Path=/; SameSite=Lax"])
source
HyperSignal.redirect_toFunction
redirect_to(location::AbstractString; cookies=String[]) -> HTTP.Response

Plain HTTP 303 redirect for non-Datastar flows (login form POST, logout, direct navigation). Pass cookies as a vector of complete Set-Cookie header values to attach session cookies to the redirect — useful for the post-login flow where you want to redirect AND set the session cookie in the same response.

For Datastar form submits that need to navigate after success, use redirect_via_fragment instead — Datastar's morph algorithm won't follow a 303.

Examples

julia> r = redirect_to("/dashboard");

julia> r.status
303

julia> Dict(r.headers)["Location"]
"/dashboard"

julia> r2 = redirect_to("/home";
                        cookies=["sid=abc; HttpOnly; Path=/"]);

julia> [v for (k, v) in r2.headers if k == "Set-Cookie"]
1-element Vector{SubString{String}}:
 "sid=abc; HttpOnly; Path=/"
source

Version pinning

HyperSignal.DATASTAR_SUPPORTED_VERSIONConstant
DATASTAR_SUPPORTED_VERSION

The Datastar protocol/client version HyperSignal is built and tested against. Pin your served datastar.js to this version; bumps land as one visible diff.

source

Datastar actions

HyperSignal.DSActionType
DSAction(verb, url, form, extras)

A "Datastar request action" — verb + URL + options. Build via ds_get, ds_post, ds_put, or ds_delete; pass to on (or its on_* shorthands) to bind it to a DOM event. The renderer formats the JS expression at the attribute boundary so the verb/URL/options live in one place.

You rarely construct one directly — use the verb constructors.

source
HyperSignal.ds_getFunction
ds_get(url; form=false, kwargs...)

Build a @get('url', {…}) Datastar action. Pass form=true to add contentType: 'form' (Datastar will encode form fields as application/x-www-form-urlencoded). Any further kwargs become {k: v} entries on the JS options object.

Examples

julia> HyperSignal.action_js(ds_get("/api/refresh"))
"@get('/api/refresh')"

julia> HyperSignal.action_js(ds_get("/api/session/count"; form=true))
"@get('/api/session/count', {contentType: 'form'})"
source
HyperSignal.ds_postFunction
ds_post(url; form=false, kwargs...)

Build a @post('url', {…}) Datastar action. Pass form=true for the common case of submitting a form; the rendered attribute is then @post('url', {contentType: 'form'}). Pass to on (or on_submit / on_click) to bind to a DOM event.

Examples

julia> HyperSignal.action_js(ds_post("/api/like"))
"@post('/api/like')"

julia> HyperSignal.action_js(ds_post("/session/new"; form=true))
"@post('/session/new', {contentType: 'form'})"
source
HyperSignal.onFunction
on(event::Symbol, action; debounce=nothing, window=false) -> Attribute

Bind a value to a DOM event. action is either a DSAction (the renderer formats it as @verb('url', {…})) or an AbstractString (a raw JS expression — useful for client-side toggles like "$open = !$open"). Returns an Attribute you drop into a tag's positional args.

Modifiers:

  • debounce=N (ms) — appends __debounce.Nms. Use for change events on inputs that should ignore mid-word typing.
  • window=true — appends __window. Routes the listener to window instead of the element, so global hotkeys reach it without focus.
  • prevent=true — appends __prevent. Calls event.preventDefault() before running action. Defaults to true for :submit so a form bound to a Datastar action doesn't also trigger the native navigation; pass prevent=false to opt out.
  • stop=true — appends __stop. Calls event.stopPropagation().
  • outside=true — appends __outside. Routes the listener to document and only fires when the event target is NOT inside the bound element (the click-outside-to-close pattern).

Examples

julia> on(:click, ds_post("/api/x"; form=true)).key
Symbol("data-on:click")

julia> on(:submit, ds_post("/save")).key                    # auto __prevent on :submit
Symbol("data-on:submit__prevent")

julia> on(:submit, ds_post("/save"); prevent=false).key     # opt-out
Symbol("data-on:submit")

julia> on(:change, ds_get("/c"); debounce=300).key
Symbol("data-on:change__debounce.300ms")

julia> on(:keydown, "$open = true"; window=true).key
Symbol("data-on:keydown__window")
source
HyperSignal.on_clickFunction
on_click(action; debounce=nothing)
on_submit(action; debounce=nothing)

Single-event shorthands for on(:click, action) / on(:submit, action). Read better than on(:click, …) in component bodies that bind exactly one event.

Examples

button("Dismiss", on_click(ds_post("/api/dismiss")))
form(on_submit(ds_post("/save"; form=true)), …)
source
HyperSignal.on_submitFunction
on_click(action; debounce=nothing)
on_submit(action; debounce=nothing)

Single-event shorthands for on(:click, action) / on(:submit, action). Read better than on(:click, …) in component bodies that bind exactly one event.

Examples

button("Dismiss", on_click(ds_post("/api/dismiss")))
form(on_submit(ds_post("/save"; form=true)), …)
source
HyperSignal.on_change_debouncedFunction
on_change_debounced(action; ms=300) -> Attribute

Shorthand for on(:change, action; debounce=ms). The default 300ms is the cadence used across this codebase for form-driven live updates — short enough to feel instant, long enough to ignore mid-word typing.

Examples

form(on_change_debounced(ds_get("/api/preview"; form=true)),
     input(type="text", name="query"))
source
HyperSignal.on_intervalFunction
on_interval(action; ms=5000) -> Attribute

Run action (a DSAction or raw JS expression) on a recurring interval. Renders as data-on-interval__duration.Nms="…". The default 5-second cadence matches the dashboard-stats polling pattern in this codebase.

data-on-interval is a Datastar plugin distinct from data-on:event — it doesn't take an event name, only a duration modifier.

Examples

section(id="dataset-stats",
    on_interval(ds_get("/api/dashboard/stats"); ms=5000),
    …)
source

Datastar attributes

HyperSignal.ds_indicatorFunction
ds_indicator() -> Attribute

Mark an element as a Datastar request indicator. The element becomes visible while a Datastar action initiated under it is in flight, and hides again on completion — Datastar adds/removes the visibility via the data-indicator attribute the renderer emits.

Examples

button("Save", on_click(ds_post("/api/save")),
    span(class="spinner", ds_indicator(), "…"))
source
ds_indicator(signal::AbstractString) -> Attribute

Mark an element as the indicator for a named in-flight signal. Datastar sets signal to true while requests under this scope are in flight, so sibling elements can ds_show("$signal") a spinner or grey out a panel without each having to track the request lifecycle themselves.

source
HyperSignal.ds_ignore_morphFunction
ds_ignore_morph() -> Attribute

Tell Datastar's morph algorithm to leave this element's subtree alone across fragment swaps. Useful for inputs the user is currently typing in or focused elements you don't want re-rendered.

Examples

input(type="text", name="search", ds_ignore_morph())
source
HyperSignal.ds_bindFunction
ds_bind(signal::AbstractString) -> Attribute

Two-way bind an input to a Datastar signal: the input's value mirrors signal, and edits flow back. Returns the data-bind="signal" attribute.

Examples

input(type="text", ds_bind("query"))
source
HyperSignal.ds_signalFunction
ds_signal(name::AbstractString, value) -> Attribute

Initialize a Datastar signal on this element. Renders as the keyed data-signals:<name>="value" form. Note Datastar's kebab→camel mapping: ds_signal("my-signal", …) creates the signal $mySignal.

Examples

div(ds_signal("count", 0), ds_text("count"))   # signal "count" starts at 0
source
HyperSignal.ds_signalsFunction
ds_signals(state) -> Attribute

Initialize a whole Datastar signals object on this element. state is anything JSON-encodable — typically a NamedTuple or Dict of signal name → initial value. Renders as data-signals='{...}' after attribute escape (the JSON's " round-trip cleanly through &quot;).

Use this in place of ds_signal when one element seeds several signals at once (e.g. a card with showDetails + confirmDialogOpen + …); the JSON encoding catches the kinds of typos that hand-written {"x": false, "y": false} strings drop into client-side silence.

Examples

julia> a = ds_signals((showDetails=false, count=0));

julia> a.value
"{\"showDetails\":false,\"count\":0}"

julia> a.key
Symbol("data-signals")
source
HyperSignal.ds_showFunction
ds_show(expr::AbstractString) -> Attribute

Show this element only when the JS expression expr is truthy. Renders as data-show="expr".

Examples

p(ds_show("count > 0"), "You have items.")
source
HyperSignal.ds_textFunction
ds_text(expr::AbstractString) -> Attribute

Set this element's text content from the JS expression expr. Renders as data-text="expr". Use this instead of templating a value into a string when the value is a Datastar signal that may change client-side.

Examples

span(ds_text("count"))    # text content tracks the signal "count"
source
HyperSignal.ds_json_signalsFunction
ds_json_signals() -> Attribute
ds_json_signals(filter::AbstractString) -> Attribute

Set this element's text content to a live, JSON-stringified view of the Datastar signal store — the standard in-page signal debugger. Drop a pre(ds_json_signals()) onto a page during development and it tracks the store reactively as signals change. Renders as the bare data-json-signals attribute (no value).

Pass filter — a Datastar filter-object JS expression such as "{include: /user/}" or "{exclude: /temp$/}" — to scope the output to matching signal names. Renders as data-json-signals="<filter>".

Examples

julia> a = ds_json_signals();

julia> (a.key, a.value)
(Symbol("data-json-signals"), true)

julia> b = ds_json_signals("{include: /user/}");

julia> b.value
"{include: /user/}"
source
HyperSignal.ds_refFunction
ds_ref(name::AbstractString) -> Attribute

Mark this element with a Datastar ref so other Datastar expressions can reach it as $<name> (e.g. $btnNext.click()). Renders as data-ref="name".

Examples

button(ds_ref("btnNext"), "Next")
# Elsewhere: data-on:keydown__window="if(event.key==='ArrowRight') $btnNext.click()"
source
HyperSignal.ds_attrFunction
ds_attr(name::AbstractString, expr::AbstractString) -> Attribute

Reactively bind a DOM attribute to a Datastar expression: as expr changes (because a signal it reads changes), the attribute updates. Renders as data-attr:NAME="expr". Truthy → attribute set; falsy → attribute removed.

Examples

# Open/close a <dialog> from a signal
dialog(ds_attr("open", "$dialogOpen"), …)

# Disable a button while a request is in flight
button(ds_attr("disabled", "$saving"), "Save")
source
HyperSignal.ds_classFunction
ds_class(name::AbstractString, expr::AbstractString) -> Attribute

Toggle a CSS class reactively. Renders as data-class:NAME="expr" — when expr evaluates truthy Datastar adds the class, when falsy it removes it. Pair-style sibling of ds_attr; use ds_class when the target is a class on class=, ds_attr when the target is any other attribute.

Examples

# Drop the `.outline` class from the active view-toggle button
button(class="grid-toggle", ds_class("outline", "$view !== 'grid'"), "Grid")
source
HyperSignal.ds_computedFunction
ds_computed(name::AbstractString, expr::AbstractString) -> Attribute

Declare a read-only derived signal computed from a Datastar expression. The computed signal name re-evaluates whenever any signal expr reads changes. Renders as data-computed:NAME="expr". Reach it elsewhere as $NAME (totals, validation flags, formatted strings). Use this instead of recomputing the same expression at every read site.

Datastar camel-cases hyphenated signal names, so ds_computed("full-name", …) is read as $fullName; prefer camelCase names to avoid surprise.

Examples

# A line-item total that tracks its inputs; read elsewhere as $total
div(ds_computed("total", "$price * $qty"),
    span(ds_text("$total")))
source
HyperSignal.ds_styleFunction
ds_style(name::AbstractString, expr::AbstractString) -> Attribute

Set an inline CSS style property reactively. Renders as data-style:NAME="expr" — Datastar evaluates expr and writes the result to element.style.NAME, keeping it in sync as the signals it reads change. The reactive-binding sibling of ds_class (toggle a class) and ds_attr (bind any attribute); reach for ds_style when the value is a dynamic dimension/color/transform that isn't expressible as a static class.

Examples

# Drive a progress bar's width from a signal (0–100)
div(class="bar", ds_style("width", "$pct + '%'"))

# Hide via inline display rather than a class
div(ds_style("display", "$hiding && 'none'"))
source
HyperSignal.ds_effectFunction
ds_effect(expr::AbstractString) -> Attribute

Run a Datastar JS effect: a side-effecting expression that re-evaluates whenever the signals it reads change. Useful for "imperative bridge" moments where you have to call a DOM method from signal state (e.g. $dialog.showModal()). Renders as data-effect="expr".

Examples

# Open or close a dialog as `$dialogOpen` changes
div(ds_effect("$dialogOpen ? $dlg.showModal() : $dlg.close()"))
source
HyperSignal.ds_initFunction
ds_init(action_or_expr) -> Attribute

Run an action or JS expression once when this element is inserted into the DOM. Pass a DSAction for an HTTP fetch, or an AbstractString for a raw JS expression. Renders as data-init="…".

Examples

# Fetch the first card on element insert
div(id="card-container",
    ds_init(ds_get("/api/review/card?session_id=$(id)")),
    …)

# Or initialise a signal from a JS computation
div(ds_signals((width=0,)), ds_init("$width = window.innerWidth"))
source

Signal decoding

HyperSignal.parse_signalsFunction
parse_signals(req_or_body) -> Dict{String, Any}

Decode the Datastar signals payload from a request body. Datastar's default action mode (@post('/x') without contentType: 'form') sends the active signals object as a JSON body. Pass either an HTTP.Request, a Vector{UInt8}, or an AbstractString — the helper normalizes the input and returns the parsed object as a Dict{String, Any}. Empty bodies map to an empty dict so a route can guard cleanly.

For form-encoded posts (@post('/x', {contentType: 'form'})), use the service's parse_form_body instead — Datastar treats form-mode and JSON-mode as distinct wire formats, and so does this lib.

Examples

function handle_increment(req::HTTP.Request)
    sig = parse_signals(req)
    n = Int(get(sig, "count", 0)) + 1
    fragment_response(div(id="counter", n), "#counter")
end
source

Component helpers

Top-level — primitives that don't pull in an app idiom:

HyperSignal.clsFunction
cls(parts...) -> String

Build a CSS class attribute string from a flexible mix of inputs. Accepts:

  • AbstractString → kept as-is (empty strings drop).
  • "name" => bool → included only when the bool is true.
  • Vector of any of the above → flattened.
  • nothing / missing → skipped.

Empty inputs collapse to "" so a class=cls(...) attribute is safe even when nothing matches. Pair values that aren't Bool raise a loud error — a typo like "active" => "yes" would otherwise silently include the class.

Examples

julia> cls("btn", "primary", "active" => true)
"btn primary active"

julia> cls("btn", "primary", "active" => false)
"btn primary"

julia> cls("btn", ["large", "rounded"], "loading" => false)
"btn large rounded"

julia> cls()
""
source

HyperSignal.Helpers

App-grade building blocks. Pull them in with using HyperSignal.Helpers: … — there is no top-level export and no deprecation shim.

HyperSignal.Helpers.radio_fieldFunction
radio_field(name::AbstractString, value::AbstractString, text::AbstractString; checked=false)

Render a <label><input type="radio" name=… value=… [checked]> text</label> — the label-around-input convention. One call replaces ~6 lines of hand-built input-and-label boilerplate per choice.

Examples

julia> render(radio_field("size", "S", "Small"))
"<label><input type=\"radio\" name=\"size\" value=\"S\"> Small</label>"

julia> render(radio_field("size", "L", "Large"; checked=true))
"<label><input type=\"radio\" name=\"size\" value=\"L\" checked> Large</label>"
source
HyperSignal.Helpers.checkbox_fieldFunction
checkbox_field(name::AbstractString, text::AbstractString; checked=false, value="on")

Render a <label><input type="checkbox" name=… value=… [checked]> text</label>. The default value="on" matches the form-encoded shape parse_form_body (and any standard form parser) expects — keep the default unless your backend explicitly wants a different value.

Examples

julia> render(checkbox_field("notify_email", "Email"))
"<label><input type=\"checkbox\" name=\"notify_email\" value=\"on\"> Email</label>"

julia> render(checkbox_field("agree", "I agree"; checked=true))
"<label><input type=\"checkbox\" name=\"agree\" value=\"on\" checked> I agree</label>"

julia> render(checkbox_field("opt", "Opt-in"; value="yes"))
"<label><input type=\"checkbox\" name=\"opt\" value=\"yes\"> Opt-in</label>"
source
HyperSignal.Helpers.text_fieldFunction
text_field(label_text::AbstractString, name::AbstractString;
           type="text", required=false)

Render a <label for=name>text</label><input type=… id=name name=name [required]> pair as a Frag. The for/id attribute pair ties them so the input keeps focus when the label is clicked, and screen readers announce the label whether or not the input is the label's child.

Defaults to type="text"; pass type="password" for a masked input. Pass required=true to mark the field as a constraint the browser enforces before allowing submit.

Examples

form(on_submit(ds_post("/login"; form=true)),
    text_field("Username", "username"; required=true),
    text_field("Password", "password"; type="password", required=true),
    button(type="submit", "Log in"))
source
HyperSignal.Helpers.form_legendFunction
form_legend(text::AbstractString; tooltip=nothing)

Render <legend class="muted">text [help-tooltip]</legend>. Pass tooltip to attach an inline help icon via help_tooltip. Without a tooltip the result is just a plain muted legend.

Examples

julia> render(form_legend("Size"))
"<legend class=\"muted\">Size</legend>"

For the with-tooltip variant the output includes a hashed id that isn't byte-stable across the tooltip text, so see the help_tooltip docstring for the structural details.

source
HyperSignal.Helpers.form_sectionFunction
form_section(label_text::AbstractString, cards...)

Wrap a list of "card" elements (typically <article>s) under a muted section header. Renders a Frag of <small class="muted form-section-label">label</small> and <div class="form-card-grid">cards…</div> — collapses the section-header + grid pattern that opens every section in this codebase's session form.

Returns a Frag (no wrapper element), so it inlines into a <form> without forcing an extra <div> you'd then have to style around.

Examples

form_section("Image Batch",
    article(fieldset(form_legend("Size"), radio_field("n", "10", "10"))),
    article(fieldset(form_legend("Source"), radio_field("src", "a", "A"))),
)
source
HyperSignal.Helpers.help_tooltipFunction
help_tooltip(text::AbstractString; icon=DEFAULT_HELP_ICON)

Render

<span class="help-trigger" tabindex="0"
      data-signals="{help_open: '', help_hover: ''}"
      data-on:mouseenter="$help_hover = '<id>'"
      data-on:mouseleave="$help_hover = ''"
      data-on:click__outside="$help_open === '<id>' && ($help_open = '')">
  <span class="help-icon-wrap"
        data-on:click="$help_open = $help_open === '<id>' ? '' : '<id>'">
    [icon]
  </span>
  <span class="help-popup" role="tooltip"
        data-show="$help_open === '<id>' || $help_hover === '<id>'">text</span>
</span>

The popup opens on hover (and closes on mouseleave) and toggles on click of the icon, staying open until the user clicks anywhere outside the trigger (datastar's __outside modifier on a document-level listener). id is a stable hash of the tooltip text so two helpers with the same copy share state — fine, since they'd say the same thing.

Tooltip text is auto-escaped so caller copy can include quotes / < / & without worry. Override icon with your own Raw/Element for projects with a different help glyph.

Most code reaches for form_legend instead — it pairs a legend with this tooltip in one call.

Examples

legend("Confidence ", help_tooltip("ML certainty range. Lower = ambiguous."))
source
HyperSignal.Helpers.preset_buttonFunction
preset_button(text::AbstractString, settings::AbstractVector{<:Pair{<:AbstractString,<:AbstractString}})

Render a "preset" button: clicking it sets each named radio input to checked (matching value), then dispatches a bubbling change event on the form so any data-on:change handler (e.g. a live-count GET) recomputes. The <button onclick="…"> JS is built once here so each preset doesn't repeat the escape-prone querySelector boilerplate.

settings is a vector of name => value pairs identifying the radios to flip.

Examples

fieldset(
    form_legend("Quick presets"),
    preset_button("Easy", ["confidence" => "all", "label_filter" => "both"]),
    preset_button("Hard", ["confidence" => "hard", "label_filter" => "iw"]),
)
source
HyperSignal.Helpers.signal_dialogFunction
signal_dialog(open_expr, body...; close_action, id=nothing, class="")

Render a <dialog> whose open/close state is mirrored to a Datastar expression. Collapses the boilerplate of pairing ds_effect("$x ? $dlg.showModal() : $dlg.close()") with a hand-rolled backdrop div and gives every dialog the same close semantics:

  • data-effect reads open_expr; truthy → el.showModal() (puts the dialog in the top layer with native focus trap, ESC, and ::backdrop), falsy → el.close(). Datastar exposes the host element as el inside expressions; this is the signals proxy, not the DOM node.
  • data-on:close runs close_action whenever the dialog closes by any means (ESC, programmatic, form method=dialog) so the bound signal stays in sync without the caller threading it through every dismiss site.
  • data-on:click checks event.target === el and runs close_action — that's the standard "click the backdrop area to close" affordance. Inner content must be wrapped in a child element so its clicks don't match (a top-level child <div> / <article> is enough).

close_action is a JS statement (no trailing semicolon needed) that restores the signal to its closed state — typically "$modal = 0" or "$confirmOpen = false". The same statement runs from both the :close listener (ESC, programmatic) and the backdrop click, so the signal converges to false/0 no matter how the user dismissed.

Examples

# Page-level lightbox indexed by an integer signal
signal_dialog("$lightbox",
    div(class="lightbox-frame",
        # Each panel ds_show-gated by the signal value
        (panel(i) for i in 1:n)...);
    close_action="$lightbox = 0", class="image-lightbox")

# Boolean-driven confirm dialog
signal_dialog("$confirmOpen",
    article(header(strong("Confirm")), p("Commit?"),
        button(on_click(ds_post("/api/commit")), "Yes"),
        button(on_click("$confirmOpen = false"), "No"));
    close_action="$confirmOpen = false", id="confirm-commit-dialog")
source

SVG inlining

Pure string transforms — no Makie dependency. patch_svg / inline_svg take any SVG string (CairoMakie's save("plot.svg", fig) output is the motivating case). A Figure / Scene / FigureAxisPlot overload of inline_svg is added by the HyperSignalMakieExt extension when any Makie backend (e.g. CairoMakie) is loaded.

HyperSignal.patch_svgFunction
patch_svg(svg::AbstractString;
          id_prefix::AbstractString = "",
          strip_size::Bool = true,
          add_class::Union{AbstractString, Nothing} = nothing,
          aria_label::Union{AbstractString, Nothing} = nothing) -> String

Rewrite an SVG document string so it can be inlined into HTML without breaking. Returns the patched SVG as a String — wrap with Raw to drop straight into a HyperSignal tree, or use inline_svg to do both in one call.

Transforms applied (each independently controlled):

  • The XML prolog (<?xml …?>) and any <!DOCTYPE …> are removed; both are invalid inside HTML and would otherwise trip the parser. HTML comments (<!-- … -->) are stripped too — anywhere in the document, not just the prolog — since backends like CairoMakie emit generator notes that add bytes without affecting the rendered figure.
  • When strip_size=true, the root <svg>'s width and height attributes are removed, leaving only viewBox so the figure scales to its CSS container. CairoMakie hard-codes px dimensions; strip them unless the page really wants the original size.
  • When id_prefix is non-empty, every id="…", url(#…), and xlink:href="#…" / href="#…" is rewritten with the prefix. This is the only safe way to inline more than one CairoMakie figure on the same page — its clip0 / glyph0 ids collide otherwise.
  • When add_class is set, the value is appended to the root <svg>'s class attribute (or a new class="…" is added).
  • When aria_label is set, role="img" and aria-label="…" are added to the root <svg> so screen readers announce the figure.

For accessibility, prefer passing aria_label over relying on surrounding text — the SVG is the figure, and screen readers traverse it in isolation.

Examples

julia> patch_svg("""<?xml version="1.0"?><svg width="800" height="600" viewBox="0 0 8 6"><g/></svg>""")
"<svg viewBox=\"0 0 8 6\"><g/></svg>"

julia> patch_svg("""<svg viewBox="0 0 1 1"><defs><clipPath id="c0"><rect/></clipPath></defs><g clip-path="url(#c0)"/></svg>""";
                 id_prefix="fig_")
"<svg viewBox=\"0 0 1 1\"><defs><clipPath id=\"fig_c0\"><rect/></clipPath></defs><g clip-path=\"url(#fig_c0)\"/></svg>"
source
HyperSignal.inline_svgFunction
inline_svg(figure; kwargs...) -> Raw

Render a Makie/CairoMakie Figure / Scene / FigureAxisPlot to SVG and inline it. Requires using CairoMakie (or any backend that emits image/svg+xml) in the caller's session — without it, the method body isn't loaded and you get a MethodError.

Keyword arguments are forwarded to patch_svg.

Examples

using CairoMakie, HyperSignal
fig = Figure(); lines(fig[1, 1], 1:10, rand(10))
div(class="plot", inline_svg(fig; id_prefix="fig1_", aria_label="Random walk"))
source

Macros

HyperSignal.@using_tagsMacro
@using_tags

Bring the Base-shadowed tag constructors (div, select, summary, mark, time) into the current module's scope. Equivalent to the explicit using HyperSignal: div, select, summary, mark, time line — saves callers from memorizing which names conflict.

Plain using HyperSignal already imports every other tag (h1, form, button, …) automatically; only the Base-shadowed set needs this.

Examples

using HyperSignal
HyperSignal.@using_tags

div(class="card", select(name="kind", option("a"), option("b")))
source