Fieldia

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

StepWhat it does
openAnother 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 · addLineA field from an expression, a field emptied, a line added to a table of lines.
check · save · resetThe form checked, or some fields of it; saved (a page of responses sends); put back as it was loaded.
goToA tab or a wizard's step, by its id.
say · askWords in a tone (info, success, warning, danger, muted); a question, No stopping the steps after it.
callOne of your app's actions, by name, with params.
closeThe dialog or panel this form was opened in, without saving it.
openUrlA web address, from an expression — "'/orders/' + name" — in a new tab, or this one with "newTab": false.
reloadThe 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 · deleteThe 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.
postWords 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

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.