Fieldia

Behaviour

What happens around the fields: the keys people press, what a save says, what a refused save shows, and how a record is locked and unlocked. Each is a viewer option, the same in plain JavaScript, React, Vue and Angular.

Keys

Ctrl+Enter (Cmd+Enter on a Mac) saves from wherever the cursor is — in a text box too, where a plain Enter starts a new line — and takes what is still being typed: a tag typed but not yet added is added first. In a dialog it is Save & Close. Inside the lines grid, Ctrl+Enter stays the grid's: it finishes a note.

With enterMovesToNext, Enter moves to the next field instead of sending the form, for screens typed through from top to bottom. A field that uses Enter itself keeps it: an open list picks its choice. The arrow keys always stay the text box's.

mountViewer(host, {
  page, dataSource,
  keys: { enterMovesToNext: true },     // saveWithCtrlEnter: false turns Ctrl+Enter off
});

A ✓ on a field filled in right

showValid: true puts a ✓ by a field's label once someone has filled it in and it would pass its checks — a required name typed, an email that is an email. It goes again when the field goes wrong. A yes/no box gets none: it is never wrong.

Where a save's progress shows

“Saving…”, “Saved”, or why not, beside Save — or, with saveStatus: 'toast', in a corner, where “Saved” goes again after a moment; or with saveStatus: 'bar', across the top of the page.

A save the server refuses

A data source tells why it refused a save by throwing saveRefused, and each kind shows where it belongs:

import { saveRefused } from '@fieldia/core';

async save(request) {
  // A fetch that fails outright is told as the network by itself.
  const response = await fetch('/api/partners', {
    method: 'POST',
    body: JSON.stringify(request.values),
  });
  if (response.status === 422) {
    // { "errors": { "email": "This email is taken" } }
    const { errors } = await response.json();
    throw saveRefused({ kind: 'fields', message: 'Check the fields', fields: errors });
  }
  if (response.status === 409) {
    const { message } = await response.json();
    throw saveRefused({ kind: 'rule', message });
  }
  return response.json();
}
KindShown
fieldsUnder each field, kept until that field changes; beside Save, “Not saved. Check: Email, Phone”; the focus on the first.
ruleA business rule, in a dialog.
networkA banner over the page — “Could not reach the server. Your changes are still here.” — with Retry. A fetch that fails is told as this by itself.
conflictSomeone else saved first — a ConflictError: who and when, what each changed, with Reload and Keep mine. See versions and conflicts.
anything elseIts message beside Save, with Retry.

A screen reader hears it at once, once: “Not saved. Check: Email” — the same when the page's own checks stop a save or a send. The fields keep their messages for when the focus reaches them.

A locked record

readonly: true shows the whole record locked: values as plain text, no Save, no prompts to type. Buttons still run, as in Odoo. editSwitch: true adds Edit, and Done, which saves what changed and locks the record again — or stays open on a refused save. viewer.setReadonly(false) unlocks it from your code; in React, Vue and Angular, changing readonly does.

A record its data source says this person may only read (can: { edit: false }) is locked the same way, and stays so: no Edit, no Save — in a dialog no Save & Close, its Discard a Close — no button whose steps save, and a line over the record saying why. See what this person may do.

In your app's own words

Fieldia's own words come in four languages. A page's words — labels, help, choices, titles, buttons, messages — can go through your app's translator, for apps that keep translations by text or by key. Record data never does.

mountViewer(host, { page, dataSource, translate: (text) => i18n.t(text) });

// or translate a page once, where you load it:
import { translatePage } from '@fieldia/core';
const french = translatePage(page, (text) => catalog[text] ?? text);

In Angular the input is [translator]: a [translate] binding goes to the element's own HTML attribute.