Manual LiveTable

LiveTable

elements man livetable Read as markdown

LiveTable gives you automatic select, insert, update, and delete methods, real-time publish/subscribe, and integration that runs all the way from the server through reactive html in the browser with optimistic UI updates. In practice that means: declare a LiveTable on the server, pass it to a page as a template attribute, and the page's template iterates it directly with e:for. The page server-renders with the rows in the initial response, so the first paint has data. When the browser inserts, updates, or deletes a row, the UI changes immediately while the server reconciles in the background. When another browser mutates the same table, the broadcast flows back over the connection and every watching browser patches the affected row in place. Adding a row to a thousand-row list updates only that row in the DOM.

Despite the name, a LiveTable is not tied to one database table, and it is not reactive SQL. It is a live, ordered set of rows the browser holds and keeps in sync with the server: the initial rows arrive in the first server render, then insert / update / delete events stream in and patch the affected rows in place. What those rows are is up to you. With zero config it mirrors a single table, the common case and where the name comes from. With a custom select its rows can be a join across several tables, an aggregate, or any filtered, ordered projection. Think of it as a live view of whatever rows you define, materialized on the client, not a live connection to one physical table.

You can declare a LiveTable with zero options. Elements infers the database table name from the variable, introspects the columns, and exposes the mutators. Layer in auth callbacks per mutator, custom handlers for joins or aggregations, scopes that partition by a column (per-room, per-user), and pluggable realtime modes as the table grows. The same primitive carries you from a list of todos to a per-room chat with role-based auth and a custom select that joins three tables.

At a Glance

The simplest declaration takes no options. Elements infers the table name from the variable (camelCase to snake_case), reads the columns from the database, and gives you auto-SQL for select / insert / update / delete.

// app/shared/services/comments.ts
import { LiveTable } from "@elements/app";

interface Comment {
  id: string;
  text: string;
  author: string;
  createdAt: Date;
}

export let comments = new LiveTable<Comment>();

Pass it to a page as a template attribute from the route handler:

// app/pages/comments/index.ts
import { comments } from "#app/shared/services/comments";

export default function route(req, res) {
  return new html({ comments });
}
<html (comments: LiveTable<Comment>, private form: { text: string } = { text: "" })>
  <ul>
    <li e:for={comment of comments}>
      <strong>{comment.author}</strong>: {comment.text}
    </li>
  </ul>

  <form onsubmit={() => {
    comments.insert(
      { text: form.text, author: session.get("userName")! },
      () => form.text = "",
    );
  }}>
    <input value={form.text}>
    <button>post</button>
  </form>
</html>

What happened:

  • The page server-renders with the current comments. First paint already has data.
  • comments.insert(...) adds the row optimistically. The UI shows it immediately.
  • The server receives the mutation, persists it, and broadcasts to every browser watching the same LiveTable.
  • Every other browser receives the broadcast and patches the row into its e:for.
  • The resetUI callback fires as soon as the optimistic change lands locally, before the server has confirmed. Use it to clear the state that fed the mutation (form inputs, selection, draft values) so the UI is ready for the next action without waiting on the round-trip.