Manual Getting Started At a Glance

At a Glance

elements man start/at-a-glance Read as markdown

One 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.ts
import { 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.ts
export 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.sql
create 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.ts
import { 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.ehtml
import { 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.