Actions and events
A button — in the page, a record's header, a stat button or a list — and the form's moments run steps: plain data, in order, never code. What only your app can do is a call of one of its own actions, by name. The viewer draws what the steps ask of the screen; your app hears what happened as events.
The Order by phone demo, in each framework, is the example below.
An order, in steps
{ "type": "button", "id": "new-customer", "label": "New customer", "steps": [
{ "do": "open", "page": "new-customer", "as": "panel",
"values": { "name": "caller" },
"into": { "customer_id": "id" },
"then": [{ "do": "say", "message": "Customer added", "tone": "success" }] }
] }
"on": {
"change": { "product": [{ "do": "call", "action": "check_stock" }] },
"beforeSave": [{ "do": "check" }, { "do": "ask", "message": "Send the order?" }],
"afterSave": [{ "do": "say", "message": "Order sent", "tone": "success" }]
}
New customer opens the page new-customer in a side panel, its name started from what was typed under “Who is calling”. Saved, the order's customer_id is set to the record it saved, named by that page's name field, and “Customer added” is said. Picking a product asks your app's check_stock; sending checks the form, then asks; once sent, it says so.
The steps
| Step | What it does |
|---|---|
open | Another page by its id, from your pages: in a dialog (the default), a panel beside the form — from its side: the end of the line unless it says start, left, right, top or bottom — or in its page, this form's place, with Back. On a record or a new one with values; once saved, into sets this form's fields from its answers (id is the record it saved) and then runs here. Closed without saving, the steps after it do not run. A list page shows only what its filter lets through, each valueFrom read from this form — this order's invoices, as a smart button opens them. |
set · clear · addLine | A field from an expression, a field emptied, a line added to a table of lines. |
check · save · reset | The form checked, or some fields of it; saved (a page of responses sends); put back as it was loaded. |
goTo | A tab or a wizard's step, by its id. |
say · ask | Words in a tone (info, success, warning, danger, muted); a question, No stopping the steps after it. |
call | One of your app's actions, by name, with params. |
close | The dialog or panel this form was opened in, without saving it. |
openUrl | A web address, from an expression — "'/orders/' + name" — in a new tab, or this one with "newTab": false. |
reload | The record loaded again, as your data source now has it: after your app changed it on its server. Your app's answer can ask for it too, with reload: true. |
archive · unarchive · duplicate · delete | The record's own operations, through your data source's archive, copy and delete, else your app's action of the same name. A copy is shown once made, what changed saved first; a record deleted gives way to the pager's next — see Around a record. |
post | Words in the record's conversation, each {field} showing its value: a note, or with "kind": "message" a message to its followers. In a page opened over a record, in that record's. |
Any step may run only when a condition holds. One that fails or is refused stops the rest: a check with problems, No to a question, a save refused, your app saying stop, a page closed unsaved. A button's confirm is asked first, once; its action, when it names one, runs last as a call.
The moments under the page's on: open (it has its values), change by field — a person's change only, never a step's or your app's, so steps never chase each other round; a saved form placed in the page is named by its answers' name — beforeSave (a stop keeps it unsaved), afterSave, and show by tab or wizard step.
Your app's answers
mountViewer(host, {
page, dataSource, pages,
onAction: async ({ action, values }) => {
if (action !== 'check_stock') return;
const { left, price } = await stockOf(values.product);
// Said as a warning; the steps after it stop.
if (!left) return { stop: 'Out of stock until November.' };
return { values: { price }, say: { message: `In stock: ${left}`, tone: 'info' } };
},
});
Return nothing to go on. values are set as a set step sets them; say is said; post is posted in the record's conversation; record shows another record in the form's place — the copy your server made, a credit note; open opens a page as an open step does; reload: true loads the record again first, as your server now has it; stop stops, its words said.
What the viewer draws
- A page opened comes from
pagesby{ id, version }— in a dialog or a side panel with the same data source, pages, language, skin and look; or in this form's place, with Back over it: saved, the form comes back and the steps go on; Back brings it back as it was, and they stop. A page your app has none of stops them, saying so. A page opened gets a host of its own, so its own steps run there, and aclosestep closes it. - Words are toasts at the foot of the screen, each in its tone, read out politely, one over another, going after a moment — not while the pointer or the focus is on one — or with their ×. A page opened in a dialog or a panel says its words through the page that opened it: they show over the dialog and outlive it, and a screen reader hears them inside it too.
- A question is the viewer's own box, Cancel and OK, or your
confirm. Escape answers No there and closes nothing under it. - A tab a step goes to is shown, and its
showsteps run, as for a tab a person picks. - A button is busy while its steps run — marked so, a ring turning beside its words — and is not run twice. A check that stops it takes the focus to the first problem.
mountViewer(host, {
page, dataSource, pages,
// Any part of it your own way; the rest stays the viewer's.
host: { say: (message, tone) => myToasts.show(message, tone) },
// A page a step opens: your router, or undefined to let the viewer open it.
onOpen: (request) => (request.as === 'page' ? router.open(request) : undefined),
});
Events
const viewer = mountViewer(host, { page, dataSource });
// by: 'person' | 'step' | 'app'
const off = viewer.on('change', ({ field, value, by }) => {});
viewer.on('save', ({ recordId, values }) => {});
viewer.on('send', ({ values }) => {}); // a page of responses
viewer.on('run', ({ id, result }) => {}); // steps ended, and how
viewer.on('step', ({ step }) => {}); // a wizard's step entered
viewer.setValues({ price: 380 }); // no change steps run
await viewer.run([{ do: 'check' }, { do: 'save' }]);
In React, onChange, onSave, onSend, onStep and onRun, beside onAction, which may answer. In Vue, @change, @save, @send, @step and @run; @action's handler may answer too. In Angular, (fieldChange) — (change) would hear the boxes' own change events — (save), (send), (step) and (run); an output cannot answer, so the answer is the [answer] input, and (action) still tells every call.