@fieldia/core 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +199 -0
  2. package/package.json +1 -1
  3. package/schema/page.schema.json +829 -125
  4. package/src/index.d.mts +1 -0
  5. package/src/index.d.ts +1 -0
  6. package/src/index.js +4 -1
  7. package/src/index.js.map +1 -1
  8. package/src/index.mjs +1 -0
  9. package/src/index.mjs.map +1 -1
  10. package/src/lib/format/actions.d.mts +692 -0
  11. package/src/lib/format/actions.d.ts +692 -0
  12. package/src/lib/format/actions.js +53 -0
  13. package/src/lib/format/actions.js.map +1 -0
  14. package/src/lib/format/actions.mjs +50 -0
  15. package/src/lib/format/actions.mjs.map +1 -0
  16. package/src/lib/format/field.js +4 -2
  17. package/src/lib/format/field.js.map +1 -1
  18. package/src/lib/format/field.mjs +4 -2
  19. package/src/lib/format/field.mjs.map +1 -1
  20. package/src/lib/format/layout.d.mts +745 -15
  21. package/src/lib/format/layout.d.ts +745 -15
  22. package/src/lib/format/layout.js +18 -3
  23. package/src/lib/format/layout.js.map +1 -1
  24. package/src/lib/format/layout.mjs +17 -2
  25. package/src/lib/format/layout.mjs.map +1 -1
  26. package/src/lib/format/layout.types.d.mts +2 -1
  27. package/src/lib/format/layout.types.d.ts +2 -1
  28. package/src/lib/format/page.d.mts +616 -3
  29. package/src/lib/format/page.d.ts +616 -3
  30. package/src/lib/format/page.js +17 -2
  31. package/src/lib/format/page.js.map +1 -1
  32. package/src/lib/format/page.mjs +17 -2
  33. package/src/lib/format/page.mjs.map +1 -1
  34. package/src/lib/format/part-look.d.mts +36 -0
  35. package/src/lib/format/part-look.d.ts +36 -0
  36. package/src/lib/format/part-look.js +23 -0
  37. package/src/lib/format/part-look.js.map +1 -0
  38. package/src/lib/format/part-look.mjs +20 -0
  39. package/src/lib/format/part-look.mjs.map +1 -0
  40. package/src/lib/format/references.d.mts +19 -0
  41. package/src/lib/format/references.d.ts +19 -0
  42. package/src/lib/format/references.js +131 -2
  43. package/src/lib/format/references.js.map +1 -1
  44. package/src/lib/format/references.mjs +131 -2
  45. package/src/lib/format/references.mjs.map +1 -1
  46. package/src/lib/record/data-source.d.mts +5 -1
  47. package/src/lib/record/data-source.d.ts +5 -1
  48. package/src/lib/record/data-source.js.map +1 -1
  49. package/src/lib/record/data-source.mjs.map +1 -1
  50. package/src/lib/record/form.d.mts +86 -18
  51. package/src/lib/record/form.d.ts +86 -18
  52. package/src/lib/record/form.js +343 -46
  53. package/src/lib/record/form.js.map +1 -1
  54. package/src/lib/record/form.mjs +343 -46
  55. package/src/lib/record/form.mjs.map +1 -1
  56. package/src/lib/record/run.d.mts +169 -0
  57. package/src/lib/record/run.d.ts +169 -0
  58. package/src/lib/record/run.js +350 -0
  59. package/src/lib/record/run.js.map +1 -0
  60. package/src/lib/record/run.mjs +345 -0
  61. package/src/lib/record/run.mjs.map +1 -0
  62. package/src/runtime.d.mts +5 -2
  63. package/src/runtime.d.ts +5 -2
  64. package/src/runtime.js +3 -1
  65. package/src/runtime.js.map +1 -1
  66. package/src/runtime.mjs +1 -0
  67. package/src/runtime.mjs.map +1 -1
package/README.md CHANGED
@@ -28,5 +28,204 @@ list of records — that your app implements;
28
28
  The page format is also a JSON Schema, for editors and other languages:
29
29
  `@fieldia/core/page.schema.json`.
30
30
 
31
+ A page's `look` sets its accent, font, spacing, corners, labels and light or
32
+ dark scheme, and, under `parts`, a look for each kind of part over it — the
33
+ settings each kind has are `PART_LOOKS`:
34
+
35
+ ```json
36
+ "look": {
37
+ "accent": "#1f7a4d",
38
+ "parts": {
39
+ "inputs": { "background": "#fff7e6", "border": "#c4320a", "corners": "round", "textSize": "large", "accent": "#c4320a" },
40
+ "choices": { "accent": "#1f7a4d", "background": "#f0f7ff", "border": "#1677ff", "corners": "soft", "textSize": "small" },
41
+ "groups": { "background": "#fbfaf7", "border": "#d6cfc2", "corners": "round" },
42
+ "buttons": { "accent": "#6941c6", "corners": "round", "textSize": "large" },
43
+ "tables": { "background": "#eef2f7", "border": "#9aa3ad", "textSize": "small" }
44
+ }
45
+ }
46
+ ```
47
+
48
+ ## A saved form placed in another
49
+
50
+ A block made once — an address, a contact person, a consent — is a page of its
51
+ own, placed in any other by its id with a `form` part:
52
+
53
+ ```json
54
+ { "type": "form", "id": "home", "page": "address", "name": "home_address", "title": "Home address" }
55
+ { "type": "form", "id": "billing", "page": "address", "version": 2, "name": "billing_address" }
56
+ ```
57
+
58
+ - `page` is the saved page's id; `version` keeps to one published version, and
59
+ left out means the latest.
60
+ - `name` is where its answers go: the values hold them under it as an object,
61
+ `{ "home_address": { "street": "12 Nile Street", "city": "Cairo" } }`, so two
62
+ copies of one form never mix. It must not be a field of the page, nor another
63
+ copy's name.
64
+ - `title` is the words over it: the saved page's own title when left out, none
65
+ when empty. `colspan`, `invisible` and `readonly` work as on a section.
66
+
67
+ Only a page of `sections` or `tabs` can be placed; a page cannot be placed in
68
+ itself. The page's own conditions cannot read inside a saved form's answers.
69
+
70
+ A form is given the saved page once the app has found it —
71
+ `form.embed(partId, page)`, as the viewer does — and returns the inner form.
72
+ From then the saved page's own required fields, answer rules, conditions and
73
+ worked-out values hold inside it: the outer form refuses to save or send while
74
+ one fails (the error under `home_address.street`, shown on the inner form beside
75
+ its field), sends only the copies on show, tells `home_address` as changed when
76
+ anything inside changes, and loads, puts back and saves the nested answers. A
77
+ data source sees each copy's answers as one JSON value under its name.
78
+
79
+ ## What a press or a moment does
80
+
81
+ A button — in the body, a record's header, a stat button or a list's actions —
82
+ runs `steps`, in order, each plain data and never code; and the page's `on`
83
+ runs them at its moments:
84
+
85
+ ```json
86
+ { "type": "button", "id": "new-customer", "label": "New customer", "steps": [
87
+ { "do": "open", "page": "customer", "as": "panel", "values": { "name": "customer_name" },
88
+ "into": { "customer_id": "id" }, "then": [{ "do": "say", "message": "Customer added", "tone": "success" }] }
89
+ ] }
90
+
91
+ "on": {
92
+ "change": { "product": [{ "do": "call", "action": "check_stock" }] },
93
+ "beforeSave": [{ "do": "check" }, { "do": "ask", "message": "Send the order?" }],
94
+ "afterSave": [{ "do": "say", "message": "Order sent", "tone": "success" }]
95
+ }
96
+ ```
97
+
98
+ - `open` another page in a `dialog` (the default), a `panel` beside this form,
99
+ or in its `page`; on a `record` (an expression giving its id) or a new one with
100
+ `values`. A panel comes from its `side`: `end` (the default: the end of the
101
+ line, the right or, right to left, the left) or `start`, as the page reads;
102
+ or `left`, `right`, `top` or `bottom`, whatever its direction — a side on a
103
+ dialog or a page in its place is refused. Once it is saved or sent, `into`
104
+ sets this form's fields from its answers (`id` is the record it saved) and
105
+ `then` runs here.
106
+ - `set` a field from an expression, `clear` one, `addLine` to a table of lines.
107
+ - `check` the form or some fields, `save` (a page of responses sends), `reset`.
108
+ - `goTo` a tab or a wizard step, `say` something in a tone, `ask` Yes or No.
109
+ - `call` one of the app's own actions by name, with `params`; `close` the dialog
110
+ or panel the form was opened in.
111
+
112
+ Any step may run only `when` a condition holds. A step that fails or is refused
113
+ stops the ones after it: a check that finds problems, No to a question, a save
114
+ that cannot be made, the app saying stop, a page closed without saving. The
115
+ moments are `open`, `change` (by field: a person's change, never a step's or a
116
+ rule's), `beforeSave` (a step that stops keeps it unsaved), `afterSave` and
117
+ `show` (by tab or wizard step id). A button may still name only an `action`,
118
+ handed to the app as before.
119
+
120
+ ### Running steps
121
+
122
+ A press runs with `form.runAction(id)`: the button's `confirm` asked first,
123
+ then its `steps`, then its `action`, when it names one, as a final `call`.
124
+ The app runs its own with `form.run(steps, { id })`. Each run ends with a
125
+ `RunResult`:
126
+
127
+ ```ts
128
+ const result = await form.run([{ do: 'check' }, { do: 'save' }]);
129
+ // { done: true }
130
+ // { done: false, stoppedAt: 0, step: { do: 'check' }, reason: 'check', message: 'Email is required' }
131
+ ```
132
+
133
+ - `reason` is why it stopped: `check` (problems found, and shown), `no` (a
134
+ question answered No), `save` (refused: the first problem, or the backend's
135
+ words), `app` (the app said stop, or threw — `error` holds what), `closed`
136
+ (a page opened and closed without saving), `cannot` (it cannot run here: no
137
+ host to open with, a field the page lacks).
138
+ - `stoppedAt` is its place among the steps run — for a step of an `open`'s
139
+ `then`, the open step's; for a button's `action`, one past its steps; none
140
+ when the `confirm` was answered No — and `step` is the step itself.
141
+
142
+ What the core cannot draw it asks of a host, which the viewer gives:
143
+
144
+ ```ts
145
+ const form = createForm({
146
+ page,
147
+ dataSource,
148
+ host: {
149
+ open: (request) => showPage(request), // { page, version?, as, side?, title?, recordId, values? } → { saved, recordId?, values?, label? }
150
+ say: (message, tone) => toast(message, tone),
151
+ ask: (message) => askYesOrNo(message), // Promise<boolean>
152
+ close: () => panel.close(), // the dialog or panel this form was opened in
153
+ show: (tab) => tabs.pick(tab), // and the viewer calls form.shown(tab), as for a tab picked by hand
154
+ },
155
+ });
156
+ ```
157
+
158
+ Without a host, `say` says nothing, `ask` asks the form's `confirm` option — or
159
+ goes on as though answered Yes, so a form run from code is never held by a
160
+ question — and `open`, `close` and a `goTo` of a tab stop with `cannot`.
161
+
162
+ An `open` step reads `record` and `values` against this form; a field named
163
+ alone goes whole, a link with its name. Once the page is saved, `into` reads
164
+ the opened form's values, `id` being the record it saved; a link set to that
165
+ record is named by the host's `label` (its page's title field), else the
166
+ values' `display_name`, else their `name`, else the id.
167
+
168
+ ### The app's actions
169
+
170
+ `onAction` gets each `call`: `{ id, action, params, recordId, values, recordIds }`.
171
+ `id` is the button's, or the moment's (`on.open`, `on.change.<field>`,
172
+ `on.beforeSave`, `on.afterSave`, `on.show.<id>`), or what `form.run` was
173
+ given (`run` when nothing); `recordIds` are the records chosen in a list, for
174
+ every call of that run. Fieldia waits for the answer, which may be an
175
+ `ActionResult`:
176
+
177
+ ```ts
178
+ onAction: async ({ action, values }) => {
179
+ if (action === 'price') return { values: { price: await priceOf(values.product_id) }, say: 'Priced' };
180
+ if (action === 'reserve' && !(await inStock(values))) return { stop: 'Out of stock' };
181
+ if (action === 'pick') return { open: { page: 'customer', as: 'panel', into: { customer_id: 'id' } } };
182
+ },
183
+ ```
184
+
185
+ `values` are set as a `set` step sets them (a link may be given by its id);
186
+ `say` is said, as words or `{ message, tone }`; `open` runs as an `open` step;
187
+ `stop` stops the steps after it, and its words, if any, are said as a
188
+ warning. Nothing answered means go on: an `onAction` that returns nothing
189
+ works as it always did.
190
+
191
+ ### The moments
192
+
193
+ - `open` runs once the form has its values: as soon as a new one is made, and
194
+ after each `load()` of a record.
195
+ - `change` runs for a person's change only — `setValue(name, value)`, and a
196
+ table's lines added, edited, moved or removed. `setValue(name, value, { by: 'app' })`,
197
+ `setValues(values)`, steps, worked-out values, `setWhen` and the data
198
+ source's onchange never run it, so steps never chase each other round.
199
+ - `beforeSave` runs before the save's own check, so it can fill in what is
200
+ asked for. A stop keeps it unsaved: `save()` gives false; a check's
201
+ problems show as the save's own would; any other stop leaves
202
+ `status: 'error'` and `saveProblem: { kind: 'stopped', reason, message }`.
203
+ - `afterSave` runs once saved or sent.
204
+ - `show` runs as a wizard's step is entered — the first once `open` has run —
205
+ and as the viewer calls `form.shown(tabId)` for a tab.
206
+
207
+ Each moment runs one at a time. Set off again while it runs, `open`, `change`
208
+ and `show` run once more after it, with the values as they are then, however
209
+ often they were set off meanwhile. `beforeSave` and `afterSave` do not run
210
+ again: a save made meanwhile — a step of their own, or the app saving inside a
211
+ call — goes ahead without them, so nothing waits on itself. A moment set off
212
+ by its own run, as a `show` whose `goTo` leads back, does not run.
213
+
214
+ ### Events
215
+
216
+ ```ts
217
+ const off = form.on('change', ({ field, value, values, by }) => {}); // by: 'person' | 'step' | 'app'
218
+ form.on('save', ({ recordId, values }) => {});
219
+ form.on('send', ({ values }) => {}); // a page of responses: the answers sent
220
+ form.on('action', ({ request, result }) => {}); // the app answered a call
221
+ form.on('run', ({ id, steps, result }) => {}); // a run ended
222
+ form.on('step', ({ step }) => {}); // a wizard's step entered
223
+ form.on('open', ({ recordId, values }) => {});
224
+ off();
225
+ ```
226
+
227
+ `subscribe` still hears every change of the form's state; `settled()` now
228
+ waits for runs too, a question still unanswered among them.
229
+
31
230
  To show a page, use [`@fieldia/viewer`](https://www.npmjs.com/package/@fieldia/viewer)
32
231
  or a framework binding. MIT licensed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fieldia/core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Headless form model for Fieldia: the page format, modifiers, validation and record state. No DOM.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://fieldia.dev",