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 to table name, camelCase
to snake_case). Every page that renders a view of comments gets cross-client
updates.
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 LiveView patches only the rows that changed. A row arriving over
the wire is one DOM insertion; an edit to a row repaints the bindings that read
the edited field and nothing else.
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 (partitions, windows, custom select, custom handlers):
livetable.