Manual Elements HTML Attributes

Attributes

elements man html/attributes Read as markdown

Typed attributes on a template constructor, reactive expression blocks, and the two-way bindings Elements installs on form controls.

Attributes go in () on the opening tag. Each attribute is public (the default) or private. Optional attributes use ?. Attributes can have initializers that reference other attributes, and declaration order does not matter. private attributes cannot be provided by callers, but they can be mutated inside the template scope.

Attributes
<html ( title: string, active: boolean = true, greeting: string = `Hello ${title}`, private toggle: boolean = false, )> <h1>{greeting}</h1> </html>

Attributes are fully typed and integrated with the codebase's tooling: rename, find-references, go-to-definition, and type checking work across templates and TypeScript alike.

Initializers Re-run

An initializer is live. When anything it reads changes, it runs again and its result replaces the attribute's value, including anything the template wrote to it since. That is what keeps greeting above in step with title.

It also means one initializer should not hold both a value derived from another attribute and state the template owns. Here live reads office, so switching office rebuilds live and the slots the handler stored are gone:

Wrong
<!-- wrong: slots resets whenever office changes --> <Booking ( office: Office, private live = { officeId: office.id, slots: [], }, )>

A handler that holds the old object and writes to it after an await writes to an object nothing renders any more. Split the two, so the derived part follows office and the owned part stays put:

Right
<Booking ( office: Office, private officeId: string = office.id, private slots: Slot[] = [], )>

Layout

A single short attribute sits on the tag line.

Short list
<Badge (label: string)>

More than one, or any attribute with a default value or a long type, goes one per line, so a reader can scan them. The ( stays on the tag line, each attribute gets a line and a trailing comma, and )> closes at the tag's indent.

Long list
<html class="page-signup" ( private handle: string = "", private password: string = "", private error: string = "", )>

The trailing comma is optional and worth writing: adding an attribute then touches one line instead of two.

Regular attributes stay on the tag line ahead of the (, and a long one goes after the ).

With regular attributes
<html class="page-todos" ( todos: LiveView<Todo>, view: View, private draft: Draft = { title: "" }, ) oninit={() => window.addEventListener("popstate", () => onPopState(view))}>

Do not hang the list off the tag name's indent:

Wrong
<!-- wrong: do not do this --> <html class="page-signup" (private handle: string = "", private password: string = "", private error: string = "")>

Every line there starts at a different column, so the constructor does not read as a list and class does not read as separate from it. The rule is the one TypeScript uses for a parameter list: open on the declaration line, one item per line, close at the declaration's indent.

Attributes are reactive automatically, including across template boundaries:

Reactive attributes
<TaskList (tasks: Task[])> <Task e:for={task of tasks} {task}/> done: {tasks.filter(t => t.done).length} </TaskList> <Task (task: Task)> <input value={task.desc}> <input type="checkbox" checked={task.done}> </Task>

Toggling the checkbox in <Task> updates the count in <TaskList> immediately.

Attributes live on the root tag only. An attribute list on a non-root element (e.g. <div (form: Form)>) is invalid.

A default takes the attribute's declared type. A union attribute takes a literal default directly, and so does a union field inside an object default:

Union defaults
<Panel (mode: Mode = "light")> <Panel (state: { mode: Mode } = { mode: "light" })>

Grouping template state into one object is the usual way to keep it mutable: a handler can mutate an object's fields and the change is live, while reassigning a plain attribute inside a handler is not (see Reactivity). The object is therefore often the right shape, and each union field in it keeps its type.

Expressions

{ ... } is a TypeScript expression. Allowed as element children and attribute values.

Expressions
<h1>{title}</h1> <a href={url}>{label}</a> <p>{users.length === 0 ? "empty" : `${users.length} users`}</p>

For raw HTML output without escaping, use raw():

Raw HTML
<div>{raw(post.bodyHtml)}</div>

Attributes

Attribute values follow normal HTML rules. String literals work. Expression blocks work.

Attribute values
<div class="card"> <div class={className}> <input type="text" required>

class accepts an array. Falsy entries drop. The rest join with spaces. Useful for stringing together static and dynamic classes.

Class array
<div class={["card", "card-" + theme, active && "active"]}>

style accepts an object.

Style object
<div style={{ background: "#fff", "font-family": "courier" }}>

Identity shortcut: <Task {title}> is <Task title={title}>. Spread: <Task {...props}>.

Form Bindings

value, checked, and group create two-way bindings. Works on <input>, <textarea>, <select>, and contenteditable elements. group is the exception: it binds a set of checkbox or radio inputs to one variable, and does not apply to <select>, which binds with value.

Form bindings
<input value={form.name}> <input type="checkbox" checked={form.subscribed}> <input type="number" value={form.age}> <textarea value={form.bio}/> <select value={form.country}> <option value="us">us</option> <option value="ca">ca</option> </select> <label><input type="checkbox" group={form.contacts} value="email"> email</label> <label><input type="checkbox" group={form.contacts} value="phone"> phone</label> <div contenteditable value={form.bio}/>

contenteditable must use value=, not text content. <div contenteditable>{body}</div> renders once and stays static. <div contenteditable value={body}> is reactive.

Read and write data, not the DOM. Calling el.value = "..." or otherwise mutating a bound input fights the runtime.

File Inputs

<input type="file"> uses the same value= binding, typed as File[]. When the user picks files, the runtime reads their bytes and assigns the bound variable. The binding runs from picker to variable, because browsers do not let script set a file input's files. Assign [] to clear the input.

services.ts
import { File } from "@elements/app"; /** @rpc */ function upload(files: File[]) { for (const f of files) { sql` insert into uploads ( name, content_type, data ) values ( ${f.name}, ${f.contentType}, ${f.data} ) `; } }
template.ehtml
<html ( private files: File[] = [], )> <form onsubmit={() => upload(files)}> <input type="file" multiple value={files}> <button type="submit">upload</button> </form> </html>

File.read() is asynchronous (the browser has to read bytes off disk). The form's submit handler automatically awaits any in-flight reads before running, so the files array is always populated by the time upload(files) is called. No state machine needed for the simple case.

File is a wire-format class (name, size, contentType, data: Uint8Array, lastModified) that serializes across @rpc. The data field maps directly to a Postgres bytea column.

Progress / preview UI: use an explicit handler. When you need the file bytes available before submit (e.g. to show a thumbnail or a "reading…" spinner), use File.read() directly inside an onchange handler. The sync-style transform makes the state machine read top-to-bottom:

Upload with progress
interface Upload { files: File[]; state: "idle" | "reading" | "ready" | "uploading" | "done"; } function onChange(e: Event, form: Upload) { form.state = "reading"; form.files = File.read(e.target as HTMLInputElement); form.state = "ready"; } function onSubmit(form: Upload) { form.state = "uploading"; upload(form.files); form.state = "done"; } <html ( private form: Upload = { files: [], state: "idle", }, )> <form onsubmit={() => onSubmit(form)}> <input type="file" multiple onchange={(e) => onChange(e, form)}> <span e:if={form.state === "reading"}>reading…</span> <ul e:if={form.state === "ready" || form.state === "uploading"}> <li e:for={f of form.files}>{f.name} ({f.size} bytes)</li> </ul> <button type="submit" disabled={form.state === "reading" || form.state === "uploading"}>upload</button> </form> </html>

The state and the files are one object so the handlers can write to them: a handler mutates form.state, while reassigning a bare state attribute from inside a function updates nothing (see Reactivity).

The two patterns interop with the same File.read() primitive: the value= binding just calls it for you on change.