Skip to content

Views

Everything you see in a JOPS app is a view: a JavaScript class that extends View. A screen is a view, a header is a view, a single row in a table can be a view. Each view owns a piece of HTML (its layout), its own methods, and a set of lifecycle methods that the framework calls at the right moments.

If you have built Android apps, the model will feel familiar: a view is like an Activity or a custom View class, with onLayout(), onResume(), onPause() and onBackPressed().

src/Home.js
import { View, layout } from "jops-core";
export default class Home extends View {
constructor() {
super();
this.layout = layout`
<section class="page">
<h1 id="title"></h1>
<button jops-event-bind onclick="onHello">Say hello</button>
</section>`;
this.inflate();
}
onLayout() {
this.get("title").set("Home");
}
onHello() {
this.get("title").set("Hello!");
}
}

Three things happen here:

  • The constructor sets this.layout with the layout tag and calls this.inflate(), which turns the HTML string into DOM elements.
  • onLayout() runs once the view is in the page. It fills in the title.
  • The button’s onclick="onHello" names a method of the class. JOPS wires it up for you (see Event binding).

Nothing in JOPS is reactive. Changing a property does not update the page; you update the DOM yourself with this.get(id).set(...), or rebuild subviews.

new View(layout?, props?) takes two optional arguments:

  • layout: an inline HTML string, a path to an .html file, or a path to a .js layout module. It is passed to loadLayout() when the view is created. An inline string is assigned to this.layout immediately.
  • props: the view’s input props, for a view created in code.

Most subclasses call super() with no arguments, because the layout comes from the tag’s layout= attribute, from an overridden loadLayout(), or from this.layout set in the constructor. Passing an inline layout to super() is handy when a view builds its markup from data, such as one row per record:

src/QuoteRow.js
import { View, layout } from "jops-core";
export default class QuoteRow extends View {
constructor(quote) {
super(layout`<tr><td>${quote.number}</td><td>${quote.companyName}</td></tr>`);
this.quote = quote;
}
}
// in the parent view
await this.addSubView("rows", new QuoteRow(quote));

A plain new View(html) with no subclass is fine for static content, such as the root layout of an app.

The framework calls these methods in order. All of them are optional, and the on* hooks may be async.

  1. loadLayout(path?) loads the layout: a .js module, an .html file or an inline string. Override it to choose the layout in code (see Overriding loadLayout()). A view loads its layout once.

  2. inflate() turns the this.layout string into DOM elements: this.domNode (the first top-level element) and this.domNodes (all of them).

  3. onLayout() runs once, when the view’s DOM is in the page. Add subviews, fill in initial content, fetch data, subscribe to events.

  4. onResume() runs when the view comes to the front: on first display, every time you navigate back to it, and when the same route is shown again with new parameters. Refresh data that may have changed while it was hidden, and read route parameters here.

  5. bindEvents() wires the on* attributes of jops-event-bind elements to methods of the class. You rarely override it.

Two more hooks are called by the Router:

Hook When it runs
onPause() A different view comes to the front and this one stays alive: another route is shown, a stacked view opens on top with navigate(), or replace() shows another view. It always runs before the incoming view’s onResume(). It is not called when the view is destroyed instead.
onBackPressed() The user navigates back from this view: browser Back, Router.back(), or the Android back button in hybrid apps. Never on forward navigation. For a stacked view it is the last call before the view is destroyed, so closing logic belongs here.
src/Clock.js
import { View, layout } from "jops-core";
export default class Clock extends View {
constructor() {
super();
this.layout = layout`<p id="time"></p>`;
this.inflate();
}
onResume() {
this.tick();
this.timer = setInterval(() => this.tick(), 1000);
}
onPause() {
clearInterval(this.timer); // stop work that only matters while visible
}
tick() {
this.get("time").set(new Date().toLocaleTimeString());
}
}

For the exact calls on every kind of navigation, see the navigation and lifecycle table.

Views nest. A parent view holds its children in this.subViews, and there are two ways to add them.

Any element with the jops attribute in a layout becomes a view. The tag name is only a label; src (a module whose default export is the class) or layout (a plain View with that layout) decides what is created:

src/layout/main.html
<div class="app">
<Header jops src="/src/Header.js"></Header>
<View jops layout="/src/layout/footer.html"></View>
</div>

Declared views are resolved recursively: a declared view’s own layout may declare more views, and so may a route’s layout. Other attributes on the tag become the view’s props.

Lists and anything built from data use the subview methods:

await this.addSubView("items", new TaskItem(task)); // append into the element with id="items"
await this.insertSubView(view, 0); // insert at a child index of this.domNode
this.removeSubView(view); // remove it and call view.destroy()
this.removeSubViews(); // remove every direct subview
this.removeSubViews((v) => v instanceof TaskItem); // remove only the matching ones

addSubView() and insertSubView() run the child’s full lifecycle: its layout is loaded and inflated, the views declared in its layout are rendered, then onLayout(), onResume() and bindEvents() run. The container passed to addSubView() is looked up like get(): in every top-level element of the view’s layout, in order, and it may be a top-level element itself. If no element has that id, JOPS logs a warning and adds nothing.

A common pattern: remove the old item views, then add new ones.

showItems() {
this.removeSubViews((v) => v instanceof TaskItem); // only the items: other subviews stay
for (const task of Tasks.all()) this.addSubView("items", new TaskItem(task));
}

Views whose layout is inflated in their constructor are attached right away, so the loop does not need await. Awaiting inside the loop lets two overlapping rebuilds interleave and duplicate rows.

removeSubViews() acts on direct subviews only, and is not available on a Router (its subviews are its routes; it logs a warning).

find() searches the whole subview tree, depth first, and returns every view that matches a predicate:

const rows = this.find((v) => v instanceof QuoteRow);

Called with a string, find(id) returns elements instead of views; see Elements.

The root view is the one whose render() is called first, usually in App.js. It renders into its container: <body>, the element named by data-root, or the element passed to mount().

src/App.js
import { View } from "jops-core";
export default class App {
constructor() {
const root = new View();
root.loadLayout("/src/layout/main.html").then(() => root.render());
}
}

render() resolves all declared views in the layout, recursively, so you call it once, on the root. Children are added with declared tags or addSubView(), never with their own render().

The root view gets the full lifecycle too (onLayout(), onResume(), bindEvents()), after its declared children. Its container is borrowed: destroying the root empties the container instead of removing it from the page.

destroy() removes a view for good: it destroys all of its subviews, removes its elements from the DOM, removes its owned EventBus subscriptions and clears its references. removeSubView() calls it for you.

A destroyed view cannot be shown again. Adding the same instance a second time logs a warning and does nothing. Keep important data outside the view (the Store, the parent’s fields, or the server) and create a new instance when you need to show it again.

Override destroy() to clean up things JOPS does not know about, such as timers or global subscriptions, and always call super.destroy():

destroy() {
clearInterval(this.timer);
super.destroy();
}