Data sources
Fieldia has no backend of its own. A page's records and responses travel through a data source: an object your app writes, with up to five methods. Implement only the ones your pages use.
interface DataSource {
load?(request: LoadRequest): Promise<Values>; // read a record
save?(request: SaveRequest): Promise<SaveResult>; // create or update it
onchange?(request: OnchangeRequest): Promise<OnchangeResult>; // recalculate after a change
search?(request: SearchRequest): Promise<RelatedRecord[]>; // find records to link to
submit?(request: SubmitRequest): Promise<SubmitResult>; // store one response
}
| Method | Called when | Needed for |
|---|---|---|
load | A record page opens with a recordId | Editing an existing record |
save | Someone presses Save (or autosave fires) | Record pages |
onchange | A value changes, so the backend can fill in others | Totals, defaults that depend on other fields |
search | Someone types in a many2one, many2many or reference | Links to other records |
submit | Someone sends a responses page | Surveys, sign-ups |
What each method receives
load({ model, id, fields })— the page's fields come along, so you can read exactly the columns the page shows.save({ model, id, fields, changes, values })—idisnullfor a new record.changesholds only what changed since the record was loaded;valuesholds everything, for backends that save whole records. Return{ id }, andvaluesif your backend recalculated anything.onchange({ model, id, changed, values })— return{ values }for the fields to update, and an optionalwarningto show.search({ model, query, filter, limit })— the field's filter arrives with everyvalueFromalready replaced by its value. Return[{ id, label }].submit({ pageId, values })— the answers to the questions that were shown; skipped steps are left out.
A slow answer is handled for you: if someone keeps typing, an onchange or search answer that arrives after a newer question is ignored.
What a save sends
changes.values has the plain fields that changed. Tables of lines and many2many links come as operations, named the way Odoo names its x2many commands, so an Odoo or Flectra adapter maps them one to one:
changes.lines['child_ids'] = [
{ op: 'create', key: 'new-1', values: { name: 'Sara' } },
{ op: 'update', id: 7, values: { email: 'sara@example.com' } },
{ op: 'delete', id: 9 },
];
changes.links['tag_ids'] = [{ op: 'link', id: 3 }, { op: 'unlink', id: 4 }]; // or { op: 'set', ids }, { op: 'clear' }
An adapter for a REST API
import type { DataSource } from '@fieldia/core';
export const api: DataSource = {
async load({ model, id }) {
return (await fetch(`/api/${model}/${id}`)).json();
},
async save({ model, id, values }) {
const response = await fetch(id === null ? `/api/${model}` : `/api/${model}/${id}`, {
method: id === null ? 'POST' : 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(values),
});
const saved = await response.json();
return { id: saved.id, values: saved };
},
async search({ model, query, limit = 8 }) {
const rows = await (await fetch(`/api/${model}?q=${encodeURIComponent(query)}&limit=${limit}`)).json();
return rows.map((row: { id: number; name: string }) => ({ id: row.id, label: row.name }));
},
async submit({ pageId, values }) {
await fetch(`/api/responses/${pageId}`, { method: 'POST', body: JSON.stringify(values) });
return {};
},
};
The memory data source
createMemoryDataSource() implements all five methods in memory. It is what the demos and tests use, and a good way to try a page before a backend exists. You can seed it with records, search labels and onchange rules:
import { createMemoryDataSource } from '@fieldia/core';
const dataSource = createMemoryDataSource({
records: { partner: { 1: { name: 'Nile Towers', is_company: true } }, country: { 63: { name: 'Egypt' } } },
onchange: { partner: { is_company: (values) => ({ title: values.is_company ? null : values.title }) } },
delayMs: 300, // answer slowly, to see loading states
});
dataSource.responses; // every submitted response, for tests