Layouts
A layout is the HTML of a view. It is plain HTML: no JSX, no template language, class= instead of className=. JOPS turns it into DOM elements when the view is inflated, and any jops tags inside it become subviews.
You can keep a layout in three places, and pick whichever fits the view.
Three ways to write a layout
Section titled “Three ways to write a layout”Set this.layout in the constructor with the layout tagged template, then call this.inflate(). Best for components and views built from data.
import { View, layout } from "jops-core";
export default class Card extends View { constructor(title) { super(); this.title = title; this.layout = layout`<article class="card"><h3>${title}</h3></article>`; this.inflate(); }}A plain HTML fragment in its own file, with no import or export. Best for larger screens.
<section class="profile"> <h1 id="name"></h1> <button jops-event-bind onclick="onSave">Save</button></section><Profile jops src="/src/Profile.js" layout="/src/layout/profile.html"></Profile>A module whose default export is a layout string.
import { layout } from "jops-core";
export default layout`<article class="card"><h3 id="title"></h3></article>`;import { View } from "jops-core";
export default class Card extends View { async loadLayout() { await super.loadLayout("/src/layout/card.js"); }}The layout tag escapes values
Section titled “The layout tag escapes values”layout is a tagged template literal. Every ${value} you interpolate is HTML-escaped (&, <, > and "), so data from users or the network cannot inject markup:
import { layout } from "jops-core";
const name = '<img src=x onerror="alert(1)">';layout`<p>Hello, ${name}</p>`;// <p>Hello, <img src=x onerror="alert(1)"></p>null and undefined render as empty, and layout returns a plain string, so you can use it anywhere you need safe HTML, including with Element.set().
layout`<li class="${task.done ? "done" : ""}">${task.title}</li>`raw() for trusted HTML
Section titled “raw() for trusted HTML”To insert HTML that you trust, wrap it in raw(). It is passed through as-is:
import { layout, raw } from "jops-core";
layout`<p>${raw("<b>trusted</b>")}</p>`;A common use is a list inside a single template: escape each item with layout, join them, and wrap the result in raw():
const html = layout`<ul>${raw(items.map((i) => layout`<li>${i}</li>`).join(""))}</ul>`;Use subviews instead when the items need their own events or state.
HTML file layouts
Section titled “HTML file layouts”loadLayout() accepts a path to a plain .html file. JOPS fetches the file, keeps it in an in-memory cache for the lifetime of the page, and uses its contents as the view’s layout.
There are two ways to use one:
<!-- declarative: the layout attribute on a jops tag --><View jops layout="/src/layout/header.html"></View>// programmatic: load, then render the rootconst root = new View();await root.loadLayout("/src/layout/main.html");root.render();The layout attribute works with .js layout modules too. On a tag that also has src, the class comes from src and the layout from layout.
Overriding loadLayout()
Section titled “Overriding loadLayout()”A view class can choose its own layout by overriding loadLayout(). When you pass a path, you must return or await the call to super.loadLayout():
class QuoteRow extends View { loadLayout() { return super.loadLayout("/quotes/layout/quote_row.html"); // returns the promise }}
// Equivalent, and handy when you do more after loading:class QuoteRow extends View { async loadLayout() { await super.loadLayout("/quotes/layout/quote_row.html"); }}JOPS waits for the promise that loadLayout() returns before it calls inflate(). Loading a path is asynchronous (fetch() for .html, a dynamic import() for .js), so without return or await the layout arrives too late and the view renders empty.
Passed to super.loadLayout() |
Loading | await needed |
|---|---|---|
An .html path |
fetch() |
yes |
A .js path |
dynamic import() |
yes |
| A string (inline HTML, or a layout module imported at the top of the file) | synchronous | no |
Because props are set before the layout loads, an override can pick a layout from them:
class Badge extends View { loadLayout() { return super.loadLayout(`/src/layout/badge_${this.props.variant}.html`); }}A layout loads once per view
Section titled “A layout loads once per view”loadLayout() loads once per view. Calls made while a load is in progress, with or without a path, wait for it; once a layout is loaded, later calls return at once. A failed load (for example a 404) leaves the view without a layout, so it can be retried.
This is also why new View("/path/to/layout.html") followed by addSubView() works: addSubView() waits for the layout that the constructor started loading.
Several top-level elements
Section titled “Several top-level elements”A layout does not need a single wrapper element. This is a valid layout:
<h2 id="title">Quotes</h2><p id="summary"></p>this.domNode is the first top-level element and this.domNodes holds all of them. get(), find(id), the addSubView() container lookup, event binding, show(), hide() and destroy() act on all of them, and a view added with addSubView() or insertSubView() has all of its top-level elements placed, in order. Top-level text is ignored.
Two things to keep in mind:
- Inserting by position:
insertSubView(view, index)counts the children of the first top-level element.addSubView(containerId, view)finds its container in any top-level element (or a top-level element itself), likeget(). - Animations: the Router’s
slideandfadeanimations need single-root views. A stacked view with several top-level elements switches instantly, unless the Router uses screens mode, which animates them too.