RPC and LiveTable in Templates
elements man html/integration Read as markdownCalling an @rpc from a template and rendering a LiveTable, the two places
html meets the rest of the framework.
RPC + HTML
Mark a function @rpc to call it from the browser. The compiler wires the
network call.
/** @rpc */
function saveUser(form: Form): User {
return sql<User>(`insert into users (name, age) values (${form.name}, ${form.age}) returning *`).firstOrThrow("insert returned no row");
}
Call it like a normal function from a handler.
<form onsubmit={() => saveUser(form)}>
<input value={form.name}>
<input type="number" value={form.age}>
</form>
Elements primitives (sql, @rpc, tx, session, Channel, LiveTable) are
sync-style: you write them without async/await. The compiler converts them
to async at build time and propagates the conversion up the call stack. If you
want the async version directly, import the xAsync variant (sqlAsync,
txAsync, insertAsync).
let user = saveUser(form);
let users = sql<User>(`select * from users`);
When you genuinely need explicit async (third-party libraries like the Stripe
SDK, fetch, navigator.clipboard), declare the function async and await
those specific calls. The build leaves manual async/await alone.
LiveTable + HTML
LiveTable provides crud and realtime for any reactive data source. The source can be a database table, a view, or anything else you can read and write. By default a LiveTable binds to a database table, but you can override the crud handlers to back it with whatever you want.
let comments = new LiveTable<Comment>();
This binds to the comments db table (variable name → table name, camelCase to
snake_case). Realtime is on by default, so every page that uses comments gets
cross-client updates. Pass realtime: false for a snapshot-only read.
In the template, iterate with e:for:
<ul>
<li e:for={comment of comments}>{comment.text}</li>
</ul>
Mutate from event handlers. insert, update, and delete take an optional
resetUI callback whose only job is to clear local UI state (form fields,
drafts, edit toggles) after the optimistic write. Use it to reset UI, nothing
else. Put navigation like redirect() in the handler flow, not in resetUI:
comments.insert({ text: form.text }, () => form.text = "");
comments.update({ ...comment, text: editText }, () => editing = false);
comments.delete(comment);
All three are sync-style with optimistic UI. The browser updates immediately. The server reconciles afterward. On failure, the optimistic change rolls back.
e:for over a LiveTable does O(1) updates per notify message. No re-render of
the full list.
Optimistic inserts must include every field the template displays. The
optimistic row holds only the object you pass. If the template shows
comment.author, insert({ text }) will render undefined for author until
the server reconciles. Include displayed fields at insert time even if the
server canonicalizes them. Server-generated columns the template reads count
too: a displayed createdAt (default now()) is undefined on the optimistic
row, so new Date(undefined) renders Invalid Date until the broadcast lands.
Pass a client-side placeholder like createdAt: new Date().
Full LiveTable details (scopes, custom select, custom handlers, realtime modes):
livetable.