Client Object Model
Every element of a console page has a handle in the browser: the page, its tabs, each card, a table, its ribbon and ribbon items, rows, a row's detail and the detail's tabs, switchers and charts. A handle is a plain JavaScript object. You find it, read its state, change its label or badge, refresh it and listen to what happens to it. The same model is built from the Java builders, from hand-written JSON, or from JavaScript alone.
The object model is window.JuneauViews.model. Its script, juneau-model.js, loads right after juneau-bus.js in the
views pack, so <@page> loads it for you.
A refresh icon on a row detail
Java names a handler. The handler is a function registered by name in the browser.
RowDetail.create("/api/pets/{id}/detail")
.action("refresh", "refresh-cw", "juneau.refresh")
.tab(DetailTab.table("Orders", orders).id("orders").on("loaded", "juneau.countLabel"));
juneau.refresh and juneau.countLabel are built in. Your own handlers register like this:
JuneauViews.handlers.register("pets.ordersCount", function (tab, event) {
tab.setLabel("Orders (" + event.payload.count + ")");
});
Handles
A handle has a path id built from its ancestors, for example page/pets/card:pets/table/row:7/detail/tab:orders.
A character outside [A-Za-z0-9._-] in an id is percent-encoded, so a path always splits on /.
| Kind | Parent | Path segment |
|---|---|---|
page | none | page/<id> |
pagetab | page | pagetab:<id> |
card | page | card:<id> |
table | card or detail tab | table |
ribbon, ribbonitem | table or card, ribbon | ribbon, item:<id> |
row | table | row:<rowKey> (row:#<index> when the table has no row id) |
rowdetail, detailtab | row, row detail | detail, tab:<id> |
switcher, view, chart | card, switcher | switcher, view:<id>, chart |
Navigation and lookup:
h.parent,h.children,h.closest(kind),h.find("row:7/detail").JuneauViews.model.get(path)returns the handle ornull.JuneauViews.model.page()returns the page.
Every handle has id, path, kind, el, spec (the element's JSON, frozen), data, attached and destroyed,
and these methods:
| Method | Does |
|---|---|
refresh() | Re-fetches or re-renders the element where its kind supports it. |
setLabel(text), setBadge(n, tone) | Change the visible label or badge. Text only, never markup. |
show(), hide(), activate() | Visibility, and selecting a tab or view. |
emit(name, detail) | Sends an event up the tree. |
on(name, fn), subscribe(topic, fn) | App listeners. Each returns an off() function. |
publish(topic, payload) | Publishes on the message bus. |
action(id, icon, handler), bind(event, handler), listen(topic, handler) | Declarative triggers (below). Each returns the handle. |
A table is also reachable as JuneauViews.tables.handle(el); the object it returns is the model's Table handle.
Changes are transient
setLabel, setBadge, show and hide change the element that exists now. When the element is drawn again (a
refresh, a table redraw that re-opens a row, a collapse and expand), the label, badge and visibility the server sent
come back, and only the handlers bound to the new element's own events run. A handler that wants its change to
survive binds to an event that recurs, usually loaded or rendered.
Detached and destroyed handles
When a table redraws, the handles of an expanded row that is still present are detached and re-attached after the row
re-opens, with the same object. A call on a detached handle does nothing and logs one E-JS-117 console warning. A
handle is destroyed when its card unmounts, when its row is gone after a draw, when its detail collapses or when you
call remove(); a destroyed handle never comes back. A table without row ids destroys its row handles on every draw.
Handlers
JuneauViews.handlers.register(name, fn) registers a handler. A name has at least one dot, such as
orders.refreshDetail. The function receives (target, event): the handle that declared the handler, and an event
with type, source, payload, meta and stopPropagation().
JuneauViews.handlers.get(name) and names() read the registry. Names starting with juneau. are reserved for the
built-ins:
| Name | Does |
|---|---|
juneau.refresh | target.refresh() |
juneau.refreshClosest | Refreshes the closest ancestor of the payload's kind (default card). |
juneau.expand, juneau.collapse | Expand or collapse a row. |
juneau.activate | target.activate() |
juneau.publish | Publishes the payload's topic and payload on the bus. |
juneau.countLabel | Writes Label (n) from the loaded event's count. |
juneau.countBadge | Writes the same count as an info badge. |
Handler names resolve after the page's scripts have loaded, so a handler may be registered anywhere in a page script.
A handler that throws or rejects shows E-JS-118 on its target only; other handlers still run.
Triggers
| Trigger | Java | JavaScript | Fires |
|---|---|---|---|
| Click on an icon button | .action(id, icon, handler) | h.action(id, icon, handler) | On click, with payload {actionId, ...}. |
| Lifecycle event | .on(event, handler) | h.bind(event, handler) | When the element loads, renders, expands or is activated. |
| Bus topic | .subscribe(topic, handler) | h.listen(topic, handler) | When a message arrives. A topic may hold {rowKey}, {tabId} or {cardId}. |
The Java builders CardSpec, TableSpec, RibbonItem, RowDetail and DetailTab, plus PageSpec for the page-level
loaded event, implement Handled, which carries these three methods. ModelAction builds an action with a label,
tooltip, tone and payload. HandlerRef.named, HandlerRef.builtin and HandlerRef.inline name a handler.
The events each kind documents are:
| Kind | Events |
|---|---|
| page | loaded |
| card, table, rowdetail, chart | loaded, rendered |
| row | rendered, expanded, collapsed |
| detailtab | loaded, rendered, activated |
| ribbonitem, pagetab, view | activated |
loaded means data arrived for the element, rendered means its DOM was painted (it fires again on every
re-attach), and activated means it became the selected tab or view. The loaded payload of a table or a nested
table tab is {count}.
emit(name, detail) walks from the handle up to the page. At each level the app listeners added with on(name, fn)
run first, then the element's own declared handler. event.stopPropagation() stops the walk. Emitted events never
touch the bus; publish does.
Page scripts and inline handlers
PageSpec.script(url...) (the same list as init) adds same-origin scripts to the page. They are emitted as
<script> tags and also listed in the page contract's scripts, so a page that is mounted from JSON loads them in
order before handler names resolve. A URL that fails to load or is not same-origin shows E-JS-124.
A handler can be written in line, as HandlerRef.inline("...") or {"inline": "..."} in the JSON. This is off by
default. It needs PageSpec.allowInlineHandlers(true), which also loads juneau-model-inline.js (the only shipped
script that compiles code), and a Content-Security-Policy that allows 'unsafe-eval' in script-src. If the
response has a Content-Security-Policy without it, rendering throws an IllegalStateException. Inline handlers are
meant for prototypes and pages with no Java; use named handlers otherwise. An inline handler that is not allowed, or
is blocked, shows E-JS-119.
Pages with no Java
A page can be built from JavaScript alone, with the same objects the Java builders describe:
const M = JuneauViews.model;
const page = new M.Page({ title: "Pets", nav: [], activeNav: [], modelVersion: "1" });
const pets = new M.Table({ id: "pets", dataUrl: "/api/pets", selection: { rowIdField: "id" },
columns: [ { key: "id" }, { key: "name" } ] });
const detail = pets.setDetail({ endpoint: "/api/pets/{id}/detail", title: "Pet {id}" });
detail.addTab({ id: "orders", title: "Orders", kind: "table", nested: ordersNested })
.bind("loaded", "pets.ordersCount");
page.add(pets);
page.mount({ root: document.getElementById("app"), document });
page.toJSON() returns the full page contract, which is valid against juneau-page.schema.json. M.fromJSON(json)
and M.build(json, {root, document}) go the other way. M.validate(json) returns the findings. The Java and the
JavaScript contracts are equal for the same page. The constructors require the ids that the bus needs (cards, tabs,
ribbon items and actions); a missing or duplicate sibling id is E-JS-121.
A page with no Java cannot:
- get a CSRF token, which the server writes into
data-juneau-csrf; mutating actions send no token unless your page supplies that attribute; - get a per-response CSP nonce;
- use server-rendered
<template data-card>content (put your own<template>elements in the document, or usesrc); - have its scripts and vendor libraries picked for it (include them in the documented order);
- use
RowDetail.fromBeanandRowDetailMixin(pointendpointat your own REST operation instead).
Card classes
A custom card type is a class that extends JuneauViews.model.Card:
class GaugeCard extends JuneauViews.model.Card {
static roles = { data: "data" }; // Subscription.as(...) role -> method
static ops = { highlight: "highlight" }; // cmd op -> method
render() { /* this.el, this.spec, this.signal */ }
refresh() { this.render(); } // optional
destroy() { } // optional
data(payload, meta) { } // the "data" role
highlight(cmd) { } // the "highlight" op, with the full cmd
}
JuneauConsole.registerCard("gauge", GaugeCard);
(window.JuneauConsoleCards ||= []).push(["gauge", GaugeCard]); // queue form, same pair
render()is required and may return a promise.this.el,this.spec(frozen) andthis.signalare set before it runs.- The old context object's members are now methods of the card:
this.document,this.template(name),this.fail(msg),this.onReady(fn),this.fetchJson(url, opts),this.isAbort(e),this.paintLoading(target),this.paintError(target, err),this.every(ms, fn),this.prefs(ns),this.onDestroy(fn),this.announce(text),this.publish(...)andthis.subscribe(...). static rolesandstatic opsmap a wire name to a method name. ASubscription.as(role)that is notrefreshorparamsmust be a key ofroles. Each value must name a method of the class, or registration fails.JuneauConsole.cardApi(id)returns the card handle.setApiis gone: the public methods of your class are the API.
Migrating from the object form
registerCard(type, { render }) no longer works. An object or a plain function raises E-JS-21, a fatal banner whose
message gives the class form. Port a card by moving render(card, el, ctx) into a class method that reads
this.spec, this.el and the members above, and by turning each handler.roles and handler.ops entry into a
static roles and static ops map. Cards that ship with the console (html, terminal-view, chart, terminal,
datatables and job-log) already use the class form.
Failures are loud
| Code | When | Shown |
|---|---|---|
E-JS-21 | registerCard is given something that is not a Card subclass, or a declared role or op method is missing | Banner |
E-JS-114 | A handler name does not match the grammar | Banner (at register); thrown (imperative) |
E-JS-115 | A handler name is registered twice | Banner |
E-JS-116 | The page names a handler that is not registered | On the element that named it |
E-JS-117 | A method is called on a detached or destroyed handle | Console warning, once per handle |
E-JS-118 | A handler threw or rejected | On the target |
E-JS-119 | An inline handler is not allowed, cannot compile or is blocked by the CSP | On the element |
E-JS-120 | modelVersion is missing where required, or unsupported | Banner |
E-JS-121 | Another model finding: missing or duplicate ids, re-parenting, an undocumented event | On the element (JSON mount); thrown (imperative) |
E-JS-122 | handlers.register is given something that is not a function | Banner |
E-JS-123 | App code uses or registers a juneau.* name that is not a listed built-in | Banner (register); on the element (page) |
E-JS-124 | A page script failed to load or is not same-origin | Banner |
E-JS-125 | The page uses model blocks or handler subscriptions but juneau-model.js is not loaded | Banner |
E-JS-126 and E-JS-127 belong to the switcher; see Switcher Card.