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().
A first view
Section titled “A first view”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.layoutwith thelayouttag and callsthis.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.
The constructor
Section titled “The constructor”new View(layout?, props?) takes two optional arguments:
layout: an inline HTML string, a path to an.htmlfile, or a path to a.jslayout module. It is passed toloadLayout()when the view is created. An inline string is assigned tothis.layoutimmediately.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:
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 viewawait 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 lifecycle
Section titled “The lifecycle”The framework calls these methods in order. All of them are optional, and the on* hooks may be async.
-
loadLayout(path?)loads the layout: a.jsmodule, an.htmlfile or an inline string. Override it to choose the layout in code (see Overriding loadLayout()). A view loads its layout once. -
inflate()turns thethis.layoutstring into DOM elements:this.domNode(the first top-level element) andthis.domNodes(all of them). -
onLayout()runs once, when the view’s DOM is in the page. Add subviews, fill in initial content, fetch data, subscribe to events. -
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. -
bindEvents()wires theon*attributes ofjops-event-bindelements 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. |
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.
Subviews
Section titled “Subviews”Views nest. A parent view holds its children in this.subViews, and there are two ways to add them.
Declared in a layout
Section titled “Declared in a layout”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:
<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.
Added in code
Section titled “Added in code”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.domNodethis.removeSubView(view); // remove it and call view.destroy()this.removeSubViews(); // remove every direct subviewthis.removeSubViews((v) => v instanceof TaskItem); // remove only the matching onesaddSubView() 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.
Rebuilding a list
Section titled “Rebuilding a list”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).
Finding views
Section titled “Finding views”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
Section titled “The root view”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().
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.
Destroyed views are final
Section titled “Destroyed views are final”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();}