At a Glance
elements man start/at-a-glance Read as markdownOne page of the whole framework: a route, a template, an RPC, a job, a LiveTable, a test.
A small real-time comment app. Most of Elements in one place: HTML
templates, a route, a migration, and a LiveTable.
index.tsimport { App } from "@elements/app"; import config from "#config"; import home from "#app/pages/home"; const app = new App(); app.route("/", home); app.start(config);
app/pages/home/models.tsexport interface Comment { id: string; text: string; selected?: boolean; }
Generated by
elements create migration "add comments table" -tables=comments. The
-tables flag scaffolds the canonical baseline (id, createdAt,
updatedAt, and the touchUpdatedAt trigger) for each named table. Pass
-tables=comments,users to scaffold several at once.
app/migrations/20260509120000-add-comments-table.migration.sqlcreate or replace function touchUpdatedAt() returns trigger language plpgsql as $$ begin new.updatedAt = now(); return new; end; $$; create table comments ( id uuid primary key default uuidGenerateV7(), text text not null, createdAt timestamptz not null default now(), updatedAt timestamptz not null default now() ); create trigger commentsTouchUpdatedAt before update on comments for each row execute function touchUpdatedAt();
id is UUIDv7: time-sortable, so newer rows sort after older ones without a
separate timestamp index. Columns are camelCase in the migration. Elements
converts to snake_case at the database boundary and back to camelCase on
results.
Save the file and the project server applies the migration to the
development database. Edit the file (add a column, an index, a
constraint) and the project server rolls back and re-applies. Once a
migration has run on staging or production it's frozen and never runs
again. Agents don't run elements db migrate or touch the database
directly.
app/pages/home/index.tsimport { Request, Response, LiveTable } from "@elements/app"; import { Comment } from "./models"; import html from "./template"; // The declaration of the `comments` table, inferred from the variable name. // It configures the table and holds no rows. Every watching browser, on every // app server, sees new rows live. const comments = new LiveTable<Comment>(); // `comments.view()` in the route below reads this request's rows and opens // the subscription. The LiveView it returns is what the template renders and // what browser code writes through. export default function route(req: Request, res: Response) { return new html({ comments: comments.view(), mode: req.params.mode ?? "light", }); }
app/pages/home/template.ehtmlimport { Layout } from "#app/shared/templates/layout"; import { Comment } from "./models"; import { LiveView } from "@elements/app"; import "./style.css"; // event handlers function onAddComment(form: { text: string }, comments: LiveView<Comment>) { comments.insert({ text: form.text }, () => form.text = ""); } // templates <html class="home" ( comments: LiveView<Comment>, mode: string = "dark", )> <Layout mode={mode}> <CommentList> <CommentItem e:for={comment of comments} {comment} /> </CommentList> <CommentAdd {comments} /> </Layout> </html> <CommentList> <ul class="comment-list"> <slot/> </ul> </CommentList> <CommentItem (comment: Comment)> <li tabindex={0} class={["comment-item", comment.selected && "comment-selected"]} onfocus={() => comment.selected = true} onblur={() => comment.selected = false}> {comment.text} </li> </CommentItem> <CommentAdd ( comments: LiveView<Comment>, private form: { text: string } = { text: "" }, )> <form class="comment-add" onsubmit={() => onAddComment(form, comments)}> <input value={form.text} placeholder="Write a comment..." /> <button type="submit">Post</button> </form> </CommentAdd>
This one example covers: App and app.route(), a migration with
id/camelCase/trigger, a page route handler returning new html({...}),
a LiveTable with select/insert/update/delete and realtime, template
attributes with private and defaults, sub-templates and <slot/>,
e:for (keyed by id), event handlers, two-way form binding with
value=, reactive class arrays, and lifecycle-style reactivity via
mutating comment.selected.