Skip to content

Elements

Because JOPS is not reactive, you update the page yourself. The tool for that is this.get(id), which returns an Element: a small wrapper around one DOM element with chainable methods.

this.get("username").set("Alice"); // write innerHTML
const current = this.get("username").value(); // read innerHTML

get(id) searches the view’s layout, across every top-level element, and returns the first element with that id. The search includes everything inside the view, so a parent’s get("title") can also find a title inside a child view’s DOM. Repeated components can reuse ids such as title inside themselves, but give parent-level elements distinct ids.

Method / property What it does
set(html) Sets the element’s innerHTML. Returns the wrapper. Not escaped.
attr(name, value) Sets an attribute. null or undefined removes it; "" sets a boolean attribute such as disabled, hidden or required. Returns the wrapper.
value() Returns the element’s current innerHTML.
element The underlying DOM element, for anything else: classList, textContent, form properties. null when no element has that id.
Element.from(element, id) Static. Wraps an element you already have.

set() and attr() return the wrapper, so calls chain:

this.get("quote_link")
.set(layout`${quote.number}`)
.attr("href", `#details?id=${quote.id}`);
this.get("save").attr("disabled", ""); // disable
this.get("save").attr("disabled", null); // enable again
this.get("empty").attr("hidden", items.length ? "" : null);

For anything the wrapper does not cover, use the DOM element directly:

this.get("status").element.classList.add("is-active");

To read form fields, plain DOM is the simplest: new FormData(event.target) or form.elements.title.value.

set() writes innerHTML and does not escape. That is what makes it useful for markup, and also what makes it dangerous with untrusted data. Pass values from users or the network through the layout tag:

this.get("greeting").set(layout`Hello, ${user.name}`); // escaped
this.get("greeting").set(`Hello, ${user.name}`); // not escaped: unsafe

Two more rules:

  • Markup you insert with set() is not bound by bindEvents(). Use event delegation for generated markup.
  • Don’t use set() to clear a container that holds subviews. Call removeSubViews() (or removeSubView() for each) first, otherwise the views stay in the parent’s subViews.

These three methods are guarded:

  • No element with that id (usually a typo): they do nothing (value() returns "") and log a warning that names the id.
  • The view was destroyed (for example, a request finished after the user had already left the screen): they do nothing, silently.

This makes late async work safe:

async onResume() {
const user = await fetch("/api/me").then((r) => r.json());
this.get("name").set(layout`${user.name}`); // harmless if the view is gone
}

Code that uses .element or this.domNode directly has no such guard, so prefer set() and attr() after an await.

find(id) returns an array of Element wrappers for every element with that id across the view’s layout, including the DOM of its subviews, in page order:

this.find("price").forEach((el) => el.attr("data-currency", "USD"));

Called with a function instead of a string, find(condition) searches the subview tree and returns views; see Views.