Your first app
Declare state, read it, change it
Section titled “Declare state, read it, change it”You clicked times.
Hello, !
<div data-signals="{ clicks: 0, name: '' }"> <button data-on:click="$clicks++">Click me</button> <p>You clicked <b data-text="$clicks"></b> times.</p> <input data-bind:name placeholder="Your name"> <p data-show="$name">Hello, <span data-text="$name"></span>!</p></div>data-signalscreates signals from a JavaScript object literal. Signals are the only client state.data-on:clickruns an expression when the event fires.$clicks++reads and writes the signal.data-textkeeps the element’s text equal to the expression. It re-runs only when a signal it read changes.data-bindis two-way: typing writes the signal, and setting the signal updates the input.data-showtogglesdisplay: none.
Expressions are plain JavaScript. $name resolves to the name signal; everything else (el, evt, globals) resolves as usual. See Expressions.
Talk to the server
Section titled “Talk to the server”Add @get and an endpoint. The endpoint reads the signals the browser sent, renders HTML, and returns it. Because the returned element has the same id as the one on the page, sigmx morphs it in place: the input keeps focus, and only the list changes.
- Aberdeen
- Bath
- Belfast
- Birmingham
- Bristol
- Cambridge
- Cardiff
- Chester
- Derby
- Dundee
- Durham
- Edinburgh
- Exeter
- Glasgow
- Inverness
- Leeds
- Leicester
- Lincoln
- Liverpool
- London
- Manchester
- Newcastle
- Norwich
- Nottingham
- Oxford
- Plymouth
- Sheffield
- Southampton
- Swansea
- York
---import { towns } from '../../data/towns';import Towns from '../partials/Towns.astro';---<div class="ex search-app" data-signals="{ q: '', results: 30 }"> <header class="bar"> <label class="search"><span aria-hidden="true">⌕</span><input class="field" type="search" placeholder="Filter towns…" data-bind:q data-on:input__debounce.250ms="@get('/api/search')" data-indicator:searching></label> <span class="chip" data-text="$searching ? 'searching…' : $results + ' towns'"></span> </header> <Towns q="" towns={towns} /></div><style is:global> .search-app { background: var(--ex-surface); } .search-app .bar { display: flex; align-items: center; gap: .8rem; padding: .7rem 1rem; border-bottom: 1px solid var(--ex-line); } .search-app .search { position: relative; flex: 1; } .search-app .search span { position: absolute; left: .7rem; top: 50%; transform: translateY(-50%); color: var(--ex-muted); } .search-app .search .field { padding-left: 2rem; } .search-app #towns { list-style: none; margin: 0; padding: .6rem 1rem; display: flex; flex-wrap: wrap; gap: .4rem; max-height: 9rem; overflow: auto; } .search-app #towns li { padding: .25rem .7rem; border-radius: 999px; background: var(--ex-surface-2); border: 1px solid var(--ex-line); font-size: .85rem; animation: msg-in .2s ease-out; } .search-app #towns li.empty { background: transparent; border: 0; color: var(--ex-muted); }</style>Any server language can answer. The response the demo receives is a short text event stream: one event carrying the re-rendered list, one carrying a signal.
event: sigmx-patch-elementsdata: elements <ul id="towns"><li>Bristol</li><li>Brighton</li></ul>
event: sigmx-patch-signalsdata: signals {"results": 2}A plain text/html response containing the same <ul id="towns"> would work as well; the stream is only needed to update signals in the same round trip. The signals the browser sent arrive in the sigmx query parameter for @get (and in the JSON body for @post), so the handler reads q from there. Nothing about this is tied to a runtime: the format is described in Talking to the server, and a template engine in any language can produce it.
For JavaScript runtimes, sigmx/server builds those responses from Fetch-API primitives. The same handler in that form:
import { patchElements, patchSignals, readSignals, sse } from 'sigmx/server'
export const GET = async (request: Request) => { const { q = '' } = await readSignals(request) const matches = towns.filter((t) => t.toLowerCase().includes(String(q).toLowerCase())) const list = `<ul id="towns">${matches.map((t) => `<li>${t}</li>`).join('')}</ul>` return sse(patchElements(list), patchSignals({ results: matches.length }))}The demo above happens to be served by an Astro route on this site; the Astro, Hono and Rust SDKs wrap the same protocol for their frameworks.
Three things happen in that one response: the <ul id="towns"> is morphed, the results signal is updated, and the data-indicator:searching signal flips back to false when the request finishes. Every other page on this site is built the same way; read on for the details.