jsonisch 0.1.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.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/form-ref-BEri3JKv.js +1123 -0
- package/dist/form-ref-D_xHSaqL.d.ts +994 -0
- package/dist/handle-submit-CD3WY9gW.js +208 -0
- package/dist/handle-submit-Dk7QW3_q.d.ts +34 -0
- package/dist/index.d.ts +724 -0
- package/dist/index.js +1521 -0
- package/dist/plugin/index.d.ts +188 -0
- package/dist/plugin/index.js +4 -0
- package/dist/plugin-BCsAE8RK.d.ts +612 -0
- package/dist/plugin-Qka-vaO1.js +1545 -0
- package/dist/react/index.d.ts +476 -0
- package/dist/react/index.js +456 -0
- package/docs/decode-fork.md +24 -0
- package/docs/plugin-lifecycle.md +44 -0
- package/docs/rerender-history.md +61 -0
- package/docs/wire-shapes.md +70 -0
- package/package.json +87 -0
|
@@ -0,0 +1,994 @@
|
|
|
1
|
+
//#region src/core/types/path.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* One step of a path: an object property key or an array index.
|
|
4
|
+
*/
|
|
5
|
+
type PathSegment = string | number;
|
|
6
|
+
/**
|
|
7
|
+
* A path from the form root to a field. Paths are runtime values — our
|
|
8
|
+
* schemas come from the database, so there is no type-level path inference.
|
|
9
|
+
*/
|
|
10
|
+
type Path = PathSegment[];
|
|
11
|
+
//#endregion
|
|
12
|
+
//#region src/core/types/schema.d.ts
|
|
13
|
+
/**
|
|
14
|
+
* A JSON-Schema node as stored in the database.
|
|
15
|
+
*
|
|
16
|
+
* Structured over the keys the store walk and codec read; everything else
|
|
17
|
+
* (including the `x-*` vocabulary: `x-column`, `x-formula`, `x-ui`,
|
|
18
|
+
* `x-relation*`, …) is reachable through the index signature. Schemas are
|
|
19
|
+
* runtime DB data — there is no compile-time value inference.
|
|
20
|
+
*/
|
|
21
|
+
interface JsonSchema {
|
|
22
|
+
/**
|
|
23
|
+
* The JSON-Schema type. May be a union list (e.g. `["string", "null"]`
|
|
24
|
+
* for nullable fields).
|
|
25
|
+
*/
|
|
26
|
+
type?: string | string[];
|
|
27
|
+
/**
|
|
28
|
+
* Object properties. Presence makes the node an object field store.
|
|
29
|
+
*/
|
|
30
|
+
properties?: Record<string, JsonSchema>;
|
|
31
|
+
/**
|
|
32
|
+
* Required property names of an object node. Properties not listed are
|
|
33
|
+
* optional and default to `undefined` instead of their empty input.
|
|
34
|
+
*/
|
|
35
|
+
required?: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Array item schema. A single schema makes the node an array field store;
|
|
38
|
+
* tuple form (an array of schemas) is not supported. An array-typed node
|
|
39
|
+
* WITHOUT items stays a value leaf whose value is the whole array (legacy
|
|
40
|
+
* relation arrays).
|
|
41
|
+
*/
|
|
42
|
+
items?: JsonSchema | JsonSchema[];
|
|
43
|
+
/**
|
|
44
|
+
* String format hint (date, email, phone, currency, …).
|
|
45
|
+
*/
|
|
46
|
+
format?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Relation reference (`schema://…`). Makes the node a relation value leaf.
|
|
49
|
+
*/
|
|
50
|
+
$ref?: string;
|
|
51
|
+
enum?: unknown[];
|
|
52
|
+
oneOf?: unknown[];
|
|
53
|
+
const?: unknown;
|
|
54
|
+
uniqueItems?: boolean;
|
|
55
|
+
additionalProperties?: unknown;
|
|
56
|
+
/**
|
|
57
|
+
* Our `x-*` vocabulary and any other JSON-Schema keywords.
|
|
58
|
+
*/
|
|
59
|
+
[key: string]: unknown;
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
//#region src/core/signal.d.ts
|
|
63
|
+
/**
|
|
64
|
+
* Reactive signal primitive for jsonisch.
|
|
65
|
+
*
|
|
66
|
+
* A deliberately tiny push-based reactivity system built for a per-component
|
|
67
|
+
* subscription model (see `jsonisch/react` `useSignalSnapshot`):
|
|
68
|
+
*
|
|
69
|
+
* - Reads made while a listener is active (via `withListener`, or a
|
|
70
|
+
* `Tracker`'s `read`) subscribe that listener to the signal.
|
|
71
|
+
* - Subscriptions are ONE-SHOT: notifying a listener consumes its
|
|
72
|
+
* subscription. Listeners are expected to re-read (and thereby
|
|
73
|
+
* re-subscribe) as a consequence of being notified — exactly what a React
|
|
74
|
+
* component does when its render re-runs.
|
|
75
|
+
* - `computed` signals are lazy and cached. Staleness propagates through the
|
|
76
|
+
* computed graph eagerly (even inside `batch`), so a read always reflects
|
|
77
|
+
* the latest written inputs; only plain listener notifications (e.g.
|
|
78
|
+
* component re-renders) are deferred by `batch`.
|
|
79
|
+
*
|
|
80
|
+
* Known trade-off (documented, not a bug): outside `batch`, a plain listener
|
|
81
|
+
* whose `notify` synchronously reads other signals may observe a stale
|
|
82
|
+
* computed across a "diamond" dependency while sibling invalidations are
|
|
83
|
+
* still propagating. The React adapter is immune because `notify` only
|
|
84
|
+
* schedules a re-render; core methods that write multiple signals should use
|
|
85
|
+
* `batch`.
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* A writable reactive signal.
|
|
89
|
+
*/
|
|
90
|
+
interface Signal<T> {
|
|
91
|
+
/**
|
|
92
|
+
* The value of the signal. Reading subscribes the active listener;
|
|
93
|
+
* writing a non-`Object.is`-equal value notifies subscribers.
|
|
94
|
+
*/
|
|
95
|
+
value: T;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* A read-only reactive signal (the shape returned by `computed`).
|
|
99
|
+
*/
|
|
100
|
+
interface ReadonlySignal<T> {
|
|
101
|
+
/**
|
|
102
|
+
* The value of the signal. Reading subscribes the active listener.
|
|
103
|
+
*/
|
|
104
|
+
readonly value: T;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A subscription target. One listener typically represents one component
|
|
108
|
+
* (React adapter) or one computed signal (internal).
|
|
109
|
+
*/
|
|
110
|
+
interface Listener {
|
|
111
|
+
/**
|
|
112
|
+
* Notifies the listener that a subscribed signal changed. Deferred (and
|
|
113
|
+
* de-duplicated) while a `batch` is active.
|
|
114
|
+
*/
|
|
115
|
+
notify: () => void;
|
|
116
|
+
/**
|
|
117
|
+
* The subscriber sets this listener is currently registered in. Used to
|
|
118
|
+
* clean up subscriptions when the listener goes away (e.g. unmount) and
|
|
119
|
+
* kept in sync by the one-shot notification cycle.
|
|
120
|
+
*/
|
|
121
|
+
subscriptions: Set<Set<Listener>>;
|
|
122
|
+
/**
|
|
123
|
+
* Optional immediate invalidation hook. When present it runs synchronously
|
|
124
|
+
* at write time, even inside `batch`. Computed signals use it to mark
|
|
125
|
+
* themselves stale and propagate invalidation through the graph so that
|
|
126
|
+
* reads inside a batch never see stale cached values.
|
|
127
|
+
*/
|
|
128
|
+
invalidate?: () => void;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Runs a function with the given listener active, restoring the previous
|
|
132
|
+
* listener afterwards (throw included). This is the ONLY way to activate a
|
|
133
|
+
* listener — a set-and-forget global (the v1 `setListener`) let one
|
|
134
|
+
* component's tracking window leak into whatever rendered next; the scoped
|
|
135
|
+
* form makes that structurally impossible.
|
|
136
|
+
*
|
|
137
|
+
* @param listener The listener to activate (or `undefined` to suspend
|
|
138
|
+
* tracking, the `untrack` case).
|
|
139
|
+
* @param fn The function whose signal reads subscribe the listener.
|
|
140
|
+
*
|
|
141
|
+
* @returns The return value of the function.
|
|
142
|
+
*/
|
|
143
|
+
declare function withListener<T>(listener: Listener | undefined, fn: () => T): T;
|
|
144
|
+
/**
|
|
145
|
+
* Returns the currently active listener, if any.
|
|
146
|
+
*
|
|
147
|
+
* Advanced/internal: exposed for tests that verify subscription bookkeeping.
|
|
148
|
+
*/
|
|
149
|
+
declare function getListener(): Listener | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* A retained tracked-read handle: `read` subscribes its owner to every
|
|
152
|
+
* signal the function touches, `dispose` drops all current subscriptions.
|
|
153
|
+
*/
|
|
154
|
+
interface Tracker {
|
|
155
|
+
/**
|
|
156
|
+
* Runs the function with the tracker's listener active and returns its
|
|
157
|
+
* result. One-shot semantics apply: a notification consumes the
|
|
158
|
+
* subscriptions, so the notified party re-reads (and thereby
|
|
159
|
+
* re-subscribes) to stay live.
|
|
160
|
+
*/
|
|
161
|
+
readonly read: <T>(fn: () => T) => T;
|
|
162
|
+
/**
|
|
163
|
+
* Drops every current subscription. The tracker stays usable — a later
|
|
164
|
+
* `read` re-subscribes.
|
|
165
|
+
*/
|
|
166
|
+
readonly dispose: () => void;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Creates a tracker: the non-React primitive the react adapter's snapshot
|
|
170
|
+
* store is built on. `onInvalidate` fires when any signal read during the
|
|
171
|
+
* last `read` changes (deferred and de-duplicated by an active `batch`).
|
|
172
|
+
*
|
|
173
|
+
* @param onInvalidate Called when a tracked signal changes.
|
|
174
|
+
*
|
|
175
|
+
* @returns The created tracker.
|
|
176
|
+
*/
|
|
177
|
+
declare function createTracker(onInvalidate: () => void): Tracker;
|
|
178
|
+
/**
|
|
179
|
+
* Creates a writable reactive signal without an initial value.
|
|
180
|
+
*
|
|
181
|
+
* @returns The created signal.
|
|
182
|
+
*/
|
|
183
|
+
declare function createSignal<T>(): Signal<T | undefined>;
|
|
184
|
+
/**
|
|
185
|
+
* Creates a writable reactive signal with an initial value.
|
|
186
|
+
*
|
|
187
|
+
* @param initialValue The initial value.
|
|
188
|
+
*
|
|
189
|
+
* @returns The created signal.
|
|
190
|
+
*/
|
|
191
|
+
declare function createSignal<T>(initialValue: T): Signal<T>;
|
|
192
|
+
/**
|
|
193
|
+
* Creates a lazy, cached, read-only signal derived from other signals.
|
|
194
|
+
*
|
|
195
|
+
* The compute function runs on first read and again after any signal it read
|
|
196
|
+
* during its last run changes (dependencies are re-tracked on every run, so
|
|
197
|
+
* conditional reads narrow or widen the dependency set). Subscribers of the
|
|
198
|
+
* computed are notified when it is invalidated; the fresh value is produced
|
|
199
|
+
* on the next read.
|
|
200
|
+
*
|
|
201
|
+
* @param compute The function deriving the value.
|
|
202
|
+
*
|
|
203
|
+
* @returns The created read-only signal.
|
|
204
|
+
*/
|
|
205
|
+
declare function computed<T>(compute: () => T): ReadonlySignal<T>;
|
|
206
|
+
/**
|
|
207
|
+
* Batches signal writes: plain listener notifications (e.g. component
|
|
208
|
+
* re-renders) are collected, de-duplicated, and delivered once when the
|
|
209
|
+
* outermost batch ends. Computed invalidation is NOT deferred, so reads
|
|
210
|
+
* inside the batch observe the written values.
|
|
211
|
+
*
|
|
212
|
+
* @param fn The function to execute in the batch.
|
|
213
|
+
*
|
|
214
|
+
* @returns The return value of the function.
|
|
215
|
+
*/
|
|
216
|
+
declare function batch<T>(fn: () => T): T;
|
|
217
|
+
/**
|
|
218
|
+
* Executes a function without tracking signal reads as subscriptions.
|
|
219
|
+
*
|
|
220
|
+
* @param fn The function to execute untracked.
|
|
221
|
+
*
|
|
222
|
+
* @returns The return value of the function.
|
|
223
|
+
*/
|
|
224
|
+
declare function untrack<T>(fn: () => T): T;
|
|
225
|
+
//#endregion
|
|
226
|
+
//#region src/core/control.d.ts
|
|
227
|
+
/**
|
|
228
|
+
* The UI-side vocabulary: what kind of widget a field renders as. Distinct
|
|
229
|
+
* from JSON-Schema `type` — e.g. "string" can map to many kinds (text,
|
|
230
|
+
* email, currency, …).
|
|
231
|
+
*
|
|
232
|
+
* Closed set, deliberately opinionated (US-lending-flavored: ein, ssn,
|
|
233
|
+
* us-state, legal-id, interest-rate, line-item); widgets are host-supplied,
|
|
234
|
+
* so unused kinds cost nothing. Add explicitly when a new widget exists.
|
|
235
|
+
*/
|
|
236
|
+
declare const CONTROL_KINDS: readonly ["text", "textarea", "number", "boolean", "date", "address", "email", "email-array", "phone", "phone-array", "link-array", "currency", "percent", "interest-rate", "ein", "ssn", "us-state", "legal-id", "select", "multiselect", "formula", "estimate", "amount-or-percent", "line-item", "object-array", "hidden"];
|
|
237
|
+
type ControlKind = (typeof CONTROL_KINDS)[number];
|
|
238
|
+
/**
|
|
239
|
+
* Decides which UI widget kind a JSON-Schema node renders as.
|
|
240
|
+
*
|
|
241
|
+
* Precedence:
|
|
242
|
+
* 1. Relations — `$ref`, array-of-`$ref`, or `x-relation`/flat
|
|
243
|
+
* `x-relation-target`. Single → select, many → multiselect.
|
|
244
|
+
* 2. Explicit `x-ui.control`, plus the `estimate: true` attribute
|
|
245
|
+
* promoting a formula to an estimate.
|
|
246
|
+
* 3. `format` — JSON-Schema standard widget hints on strings.
|
|
247
|
+
* 4. `type` — JSON-Schema primitive fallback.
|
|
248
|
+
*
|
|
249
|
+
* @param schema The JSON-Schema node.
|
|
250
|
+
*
|
|
251
|
+
* @returns The control kind.
|
|
252
|
+
*/
|
|
253
|
+
declare function inferControl(schema: JsonSchema): ControlKind;
|
|
254
|
+
//#endregion
|
|
255
|
+
//#region src/core/types/visibility.d.ts
|
|
256
|
+
/**
|
|
257
|
+
* The comparison operator of a `VisibleWhen` condition. `equals`/`one-of`/
|
|
258
|
+
* `contains` come from a schema `then` branch; the `else` branch flips them
|
|
259
|
+
* to the `not-` variants. `one-of` (LOS-822) carries an array `value` and
|
|
260
|
+
* passes while the watched value equals ANY element.
|
|
261
|
+
*/
|
|
262
|
+
type VisibleWhenOp = "equals" | "not-equals" | "one-of" | "not-one-of" | "contains" | "not-contains";
|
|
263
|
+
/**
|
|
264
|
+
* A resolved conditional-visibility rule: the field renders only while the
|
|
265
|
+
* watched field's value satisfies the condition. Resolved once at store
|
|
266
|
+
* init from the root schema's `allOf` `if/then/else` blocks; hidden values
|
|
267
|
+
* are RETAINED (visibility gates rendering, never state or payload).
|
|
268
|
+
*/
|
|
269
|
+
interface VisibleWhen {
|
|
270
|
+
/**
|
|
271
|
+
* The watched field reference: a root-level field key, or the
|
|
272
|
+
* root-record bracket form (`record[key]` by default) reading `key` off the
|
|
273
|
+
* object stored under the form's `rootRecordAlias` in the eval scope (the
|
|
274
|
+
* host layers into `offFormValues` — LOS-463/LOS-471).
|
|
275
|
+
*/
|
|
276
|
+
readonly field: string;
|
|
277
|
+
/**
|
|
278
|
+
* The comparison operator.
|
|
279
|
+
*/
|
|
280
|
+
readonly op: VisibleWhenOp;
|
|
281
|
+
/**
|
|
282
|
+
* The value the watched field is compared against.
|
|
283
|
+
*/
|
|
284
|
+
readonly value: unknown;
|
|
285
|
+
}
|
|
286
|
+
//#endregion
|
|
287
|
+
//#region src/core/types/field.d.ts
|
|
288
|
+
/**
|
|
289
|
+
* The structural kind of a field store node, mirroring the JSON-Schema
|
|
290
|
+
* shape: `properties` → object, single-schema `items` → array, everything
|
|
291
|
+
* else (including `items`-less array-typed nodes, whose whole array is the
|
|
292
|
+
* value) → value. Orthogonal to `ControlKind`, the widget vocabulary.
|
|
293
|
+
*/
|
|
294
|
+
type FieldKind = "array" | "object" | "value";
|
|
295
|
+
/**
|
|
296
|
+
* A DOM element a value field can be bound to.
|
|
297
|
+
*
|
|
298
|
+
* Type-only reference: core never touches the DOM at runtime and stays
|
|
299
|
+
* isomorphic; the react adapter populates these.
|
|
300
|
+
*/
|
|
301
|
+
type FieldElement = HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
|
302
|
+
/**
|
|
303
|
+
* The errors of a field: at least one message, or `null` when valid.
|
|
304
|
+
* Validation issues (AJV) and calc errors land in the same channel.
|
|
305
|
+
*/
|
|
306
|
+
type FieldErrors = [string, ...string[]] | null;
|
|
307
|
+
/**
|
|
308
|
+
* State shared by every field store node.
|
|
309
|
+
*/
|
|
310
|
+
interface InternalBaseStore {
|
|
311
|
+
/**
|
|
312
|
+
* The kind of field store.
|
|
313
|
+
*/
|
|
314
|
+
kind: FieldKind;
|
|
315
|
+
/**
|
|
316
|
+
* The property name of the field (last path segment as a string).
|
|
317
|
+
*/
|
|
318
|
+
name: string;
|
|
319
|
+
/**
|
|
320
|
+
* The path from the form root to the field.
|
|
321
|
+
*/
|
|
322
|
+
path: Path;
|
|
323
|
+
/**
|
|
324
|
+
* The JSON-Schema node this field was derived from.
|
|
325
|
+
*/
|
|
326
|
+
schema: JsonSchema;
|
|
327
|
+
/**
|
|
328
|
+
* The widget kind this field renders as, resolved once at walk time via
|
|
329
|
+
* `inferControl`.
|
|
330
|
+
*/
|
|
331
|
+
control: ControlKind;
|
|
332
|
+
/**
|
|
333
|
+
* Whether the field accepts a nullish value: its `type` includes `"null"`
|
|
334
|
+
* or its key is not in the parent's `required` list. Nullish fields keep
|
|
335
|
+
* `undefined`/`null` instead of defaulting to their empty input.
|
|
336
|
+
*/
|
|
337
|
+
isNullish: boolean;
|
|
338
|
+
/**
|
|
339
|
+
* The DOM elements bound to the field (react adapter only; empty on the
|
|
340
|
+
* server).
|
|
341
|
+
*/
|
|
342
|
+
elements: FieldElement[];
|
|
343
|
+
/**
|
|
344
|
+
* The elements the field registered itself (its reset baseline). Array
|
|
345
|
+
* methods move `elements` between field stores during reorders; `reset`
|
|
346
|
+
* restores each field's original elements via `elements = initialElements`.
|
|
347
|
+
* Starts as the SAME array reference as `elements` so registrations made
|
|
348
|
+
* before any reorder land in both.
|
|
349
|
+
*/
|
|
350
|
+
initialElements: FieldElement[];
|
|
351
|
+
/**
|
|
352
|
+
* The errors of the field — the ONE read channel (validation + calc).
|
|
353
|
+
* For most fields this is the same object as `validationErrors`; a
|
|
354
|
+
* formula field swaps in a computed composing `validationErrors` with
|
|
355
|
+
* its derived signal's calc error, so validation passes can never
|
|
356
|
+
* clobber a calc error (and vice versa).
|
|
357
|
+
*/
|
|
358
|
+
errors: ReadonlySignal<FieldErrors>;
|
|
359
|
+
/**
|
|
360
|
+
* The validation-sourced errors of the field. The ONLY writable error
|
|
361
|
+
* store — every writer (validate routing, `setErrors`, reset, item-state
|
|
362
|
+
* transfer) goes through this; calc errors are derived, never written.
|
|
363
|
+
*/
|
|
364
|
+
validationErrors: Signal<FieldErrors>;
|
|
365
|
+
/**
|
|
366
|
+
* Whether the field has been focused.
|
|
367
|
+
*/
|
|
368
|
+
isTouched: Signal<boolean>;
|
|
369
|
+
/**
|
|
370
|
+
* Whether the field's value has ever been changed. Unlike `isDirty` it
|
|
371
|
+
* stays `true` when the value is changed back; only a reset clears it.
|
|
372
|
+
*/
|
|
373
|
+
isEdited: Signal<boolean>;
|
|
374
|
+
/**
|
|
375
|
+
* Whether the field's value differs from its start input (semantic,
|
|
376
|
+
* empty-aware compare: `null` ≡ `undefined` ≡ `""`).
|
|
377
|
+
*/
|
|
378
|
+
isDirty: Signal<boolean>;
|
|
379
|
+
/**
|
|
380
|
+
* The conditional-visibility rule of the field, resolved once at store
|
|
381
|
+
* init from the root schema's `allOf` `if/then/else` blocks. Root-level
|
|
382
|
+
* fields only (derivation reaches into rows, visibility does not yet);
|
|
383
|
+
* absent on ungated fields.
|
|
384
|
+
*/
|
|
385
|
+
visibleWhen?: VisibleWhen | undefined;
|
|
386
|
+
/**
|
|
387
|
+
* Whether the field currently renders: a computed over the watched
|
|
388
|
+
* field's resolved value (form wins, `offFormValues` fills). Present
|
|
389
|
+
* only alongside `visibleWhen` — absent means always visible. Gates
|
|
390
|
+
* RENDERING only; a hidden field keeps its state, dirtiness, and place
|
|
391
|
+
* in the payload.
|
|
392
|
+
*/
|
|
393
|
+
visible?: ReadonlySignal<boolean> | undefined;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Presence marker for container (array/object) inputs: `true` when the value
|
|
397
|
+
* lives in the children, or the nullish value itself.
|
|
398
|
+
*/
|
|
399
|
+
type ContainerInput = true | null | undefined;
|
|
400
|
+
/**
|
|
401
|
+
* An array field store node.
|
|
402
|
+
*/
|
|
403
|
+
interface InternalArrayStore extends InternalBaseStore {
|
|
404
|
+
kind: "array";
|
|
405
|
+
/**
|
|
406
|
+
* The single item schema, validated non-tuple at walk time (so growth
|
|
407
|
+
* paths never re-trust `schema.items`).
|
|
408
|
+
*/
|
|
409
|
+
itemSchema: JsonSchema;
|
|
410
|
+
/**
|
|
411
|
+
* The child stores, one per array item.
|
|
412
|
+
*/
|
|
413
|
+
children: InternalFieldStore[];
|
|
414
|
+
/**
|
|
415
|
+
* The initial input presence (reset target; does not move with items).
|
|
416
|
+
*/
|
|
417
|
+
initialInput: Signal<ContainerInput>;
|
|
418
|
+
/**
|
|
419
|
+
* The start input presence (dirty baseline; moves with items).
|
|
420
|
+
*/
|
|
421
|
+
startInput: Signal<ContainerInput>;
|
|
422
|
+
/**
|
|
423
|
+
* The current input presence.
|
|
424
|
+
*/
|
|
425
|
+
input: Signal<ContainerInput>;
|
|
426
|
+
/**
|
|
427
|
+
* The initial item IDs (reset target; does not move with items).
|
|
428
|
+
*/
|
|
429
|
+
initialItems: Signal<string[]>;
|
|
430
|
+
/**
|
|
431
|
+
* The start item IDs (dirty baseline for length changes).
|
|
432
|
+
*/
|
|
433
|
+
startItems: Signal<string[]>;
|
|
434
|
+
/**
|
|
435
|
+
* The current item IDs, one stable ID per array item. The item count is
|
|
436
|
+
* the authoritative array length (`children` may hold stale stores past
|
|
437
|
+
* the end after a shrink, kept for baseline reuse on regrow).
|
|
438
|
+
*/
|
|
439
|
+
items: Signal<string[]>;
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* An object field store node.
|
|
443
|
+
*/
|
|
444
|
+
interface InternalObjectStore extends InternalBaseStore {
|
|
445
|
+
kind: "object";
|
|
446
|
+
/**
|
|
447
|
+
* The child stores, keyed by property name.
|
|
448
|
+
*/
|
|
449
|
+
children: Record<string, InternalFieldStore>;
|
|
450
|
+
/**
|
|
451
|
+
* The initial input presence (reset target).
|
|
452
|
+
*/
|
|
453
|
+
initialInput: Signal<ContainerInput>;
|
|
454
|
+
/**
|
|
455
|
+
* The start input presence (dirty baseline).
|
|
456
|
+
*/
|
|
457
|
+
startInput: Signal<ContainerInput>;
|
|
458
|
+
/**
|
|
459
|
+
* The current input presence.
|
|
460
|
+
*/
|
|
461
|
+
input: Signal<ContainerInput>;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* A leaf value field store node.
|
|
465
|
+
*
|
|
466
|
+
* Inputs are `unknown` — our schemas are runtime DB data, so there is no
|
|
467
|
+
* compile-time value inference.
|
|
468
|
+
*
|
|
469
|
+
* Feature state (derivation channels, envelope meta, visibility beyond
|
|
470
|
+
* the base rule) lives in plugin slots keyed by this store's identity
|
|
471
|
+
* (`FieldSlotKey`), never here — core's field shape has no compile-time
|
|
472
|
+
* dependency on any feature.
|
|
473
|
+
*/
|
|
474
|
+
interface InternalValueStore extends InternalBaseStore {
|
|
475
|
+
kind: "value";
|
|
476
|
+
/**
|
|
477
|
+
* The initial input (reset target).
|
|
478
|
+
*/
|
|
479
|
+
initialInput: Signal<unknown>;
|
|
480
|
+
/**
|
|
481
|
+
* The start input (dirty baseline; `applyBaseline` may update it).
|
|
482
|
+
*/
|
|
483
|
+
startInput: Signal<unknown>;
|
|
484
|
+
/**
|
|
485
|
+
* The current input.
|
|
486
|
+
*/
|
|
487
|
+
input: Signal<unknown>;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Any field store node.
|
|
491
|
+
*/
|
|
492
|
+
type InternalFieldStore = InternalArrayStore | InternalObjectStore | InternalValueStore;
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/core/plugin/key.d.ts
|
|
495
|
+
declare const stateBrand: unique symbol;
|
|
496
|
+
/**
|
|
497
|
+
* The identity a plugin's state is stored under in the form store's
|
|
498
|
+
* `pluginState` map (the ProseMirror pattern, minus its string-name
|
|
499
|
+
* registry). The generic is a phantom — it exists only so `getState`
|
|
500
|
+
* returns the right type; nothing is stored on the key itself.
|
|
501
|
+
*
|
|
502
|
+
* Keys are module-level singletons exported next to their plugin factory.
|
|
503
|
+
* Cross-plugin reads go through the exported key (derivation imports
|
|
504
|
+
* `envelopesKey`, never the envelopes implementation), and stay
|
|
505
|
+
* `T | undefined` — a form without the owning plugin is a legal config.
|
|
506
|
+
*/
|
|
507
|
+
declare class PluginKey<TState> {
|
|
508
|
+
readonly name: string;
|
|
509
|
+
private readonly [stateBrand];
|
|
510
|
+
constructor(name: string);
|
|
511
|
+
/**
|
|
512
|
+
* Reads the owning plugin's state off a form store, or `undefined` when
|
|
513
|
+
* the plugin is not registered on this form.
|
|
514
|
+
*/
|
|
515
|
+
getState(form: InternalFormStore): TState | undefined;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* A plugin key whose state is a per-field map keyed by field-store
|
|
519
|
+
* IDENTITY. Field stores are position-fixed — array operations move signal
|
|
520
|
+
* VALUES between stores, not the stores themselves — so per-field slots
|
|
521
|
+
* must transfer with item state via the `transferField`/`swapField` hooks,
|
|
522
|
+
* exactly like the input signals they sit next to.
|
|
523
|
+
*/
|
|
524
|
+
declare class FieldSlotKey<TSlot> extends PluginKey<Map<InternalFieldStore, TSlot>> {
|
|
525
|
+
/**
|
|
526
|
+
* Reads one field's slot, or `undefined` when the plugin is absent or the
|
|
527
|
+
* field carries no slot (a bare scalar to this plugin).
|
|
528
|
+
*/
|
|
529
|
+
get(form: InternalFormStore, store: InternalFieldStore): TSlot | undefined;
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/core/plugin/types.d.ts
|
|
533
|
+
/**
|
|
534
|
+
* A persisted wire entry split into its halves: the field's own value and
|
|
535
|
+
* the plugin-owned meta channel nested next to it.
|
|
536
|
+
*/
|
|
537
|
+
interface WireEnvelope {
|
|
538
|
+
readonly value: unknown;
|
|
539
|
+
readonly meta: Record<string, unknown>;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* A plugin's STATIC wire behavior — a plain descriptor exported next to the
|
|
543
|
+
* plugin factory, never runtime state, because the codec is isomorphic: the
|
|
544
|
+
* server imports the same descriptor for save routing (`encodeDirty`) with
|
|
545
|
+
* no form store anywhere (D7).
|
|
546
|
+
*/
|
|
547
|
+
interface WireContract {
|
|
548
|
+
/**
|
|
549
|
+
* The controls whose persisted shape is the kind-discriminated envelope.
|
|
550
|
+
* Core's decode paths unwrap these at the value leaf; everything else
|
|
551
|
+
* stays a bare value.
|
|
552
|
+
*/
|
|
553
|
+
readonly envelopeControls?: readonly string[];
|
|
554
|
+
/**
|
|
555
|
+
* Splits a raw persisted entry into its halves. Non-envelope raw (bad
|
|
556
|
+
* data) must decode defensively as `{ value: raw, meta: {} }`.
|
|
557
|
+
*/
|
|
558
|
+
readonly unwrap?: (raw: unknown) => WireEnvelope;
|
|
559
|
+
/**
|
|
560
|
+
* Returns whether a declared key's outgoing value must be dropped from
|
|
561
|
+
* the save payload entirely (`encodeDirty`) — e.g. a formula value, which
|
|
562
|
+
* is always server-recomputed.
|
|
563
|
+
*/
|
|
564
|
+
readonly skipValue?: (control: string, dirty: Record<string, unknown>, key: string) => boolean;
|
|
565
|
+
/**
|
|
566
|
+
* Normalizes a declared key's outgoing value in `encodeDirty` — the
|
|
567
|
+
* server-side enforcement twin of the client plugin's `encodeValue` (e.g.
|
|
568
|
+
* stripping the value half of an estimate envelope that does not pin
|
|
569
|
+
* `mode: "estimate"`). Returning `undefined` drops the key.
|
|
570
|
+
*/
|
|
571
|
+
readonly encode?: (control: string, raw: unknown) => unknown;
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* The context every per-form plugin hook receives: the form store and the
|
|
575
|
+
* plugin's own state (created by its `build`).
|
|
576
|
+
*/
|
|
577
|
+
interface PluginCtx<TState> {
|
|
578
|
+
readonly form: InternalFormStore;
|
|
579
|
+
readonly state: TState;
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* A jsonisch plugin: a plain object from a factory. Config lives in the
|
|
583
|
+
* factory closure; all mutable state is created inside `build`, keyed to
|
|
584
|
+
* the form store (a module-scope-hoisted plugin shared by two open forms
|
|
585
|
+
* must not cross-write). Registration happens only in the `createFormStore`
|
|
586
|
+
* config — no late registration.
|
|
587
|
+
*
|
|
588
|
+
* Every hook is sync — passes run inside `batch(() => untrack(() => …))`
|
|
589
|
+
* and an `await` there would let React render a half-rebased form.
|
|
590
|
+
*
|
|
591
|
+
* Core owns the phases: each hook is a pinned call site in core (a plugin
|
|
592
|
+
* can never sort itself before the value rebase). Plugins only order among
|
|
593
|
+
* themselves — array order within each hook, validated by `dependsOn`.
|
|
594
|
+
*/
|
|
595
|
+
interface JsonischPlugin<TState = unknown> {
|
|
596
|
+
/**
|
|
597
|
+
* The plugin name (error attribution + uniqueness).
|
|
598
|
+
*/
|
|
599
|
+
readonly name: string;
|
|
600
|
+
/**
|
|
601
|
+
* The identity the plugin's state is stored under. Exported as a
|
|
602
|
+
* module-level singleton so other plugins can read this plugin's state
|
|
603
|
+
* without importing its implementation.
|
|
604
|
+
*/
|
|
605
|
+
readonly key: PluginKey<TState>;
|
|
606
|
+
/**
|
|
607
|
+
* Keys of plugins that must be registered EARLIER in the plugins array.
|
|
608
|
+
* `createFormStore` throws when one is missing or later — turning
|
|
609
|
+
* ordering comments into hard startup errors (the estimate-pin data-loss
|
|
610
|
+
* scenario of D2).
|
|
611
|
+
*/
|
|
612
|
+
readonly dependsOn?: readonly PluginKey<unknown>[];
|
|
613
|
+
/**
|
|
614
|
+
* The plugin's static wire contract, collected onto the form store so
|
|
615
|
+
* core's decode paths can unwrap envelopes. The same descriptor object is
|
|
616
|
+
* exported next to the factory for the server-side `encodeDirty`.
|
|
617
|
+
*/
|
|
618
|
+
readonly wire?: WireContract;
|
|
619
|
+
/**
|
|
620
|
+
* 1 — creates the plugin's state container, BEFORE the schema walk (so
|
|
621
|
+
* the walk can dispatch `buildScope` for the rows it creates). No tree
|
|
622
|
+
* access here — scope work belongs in `buildScope`.
|
|
623
|
+
*/
|
|
624
|
+
build(form: InternalFormStore, config: FormConfig): TState;
|
|
625
|
+
/**
|
|
626
|
+
* 2 — wires one object scope, in plugin array order: the form root
|
|
627
|
+
* (after the walk, with the raw initial input) and every array-item
|
|
628
|
+
* object AS THE WALK CREATES IT (with the raw row object). A row created
|
|
629
|
+
* by an insert or a whole-array write behaves exactly like one the
|
|
630
|
+
* record loaded with.
|
|
631
|
+
*/
|
|
632
|
+
buildScope?(ctx: PluginCtx<TState>, scope: InternalObjectStore, raw: unknown): void;
|
|
633
|
+
/**
|
|
634
|
+
* 3 — re-seeds an EXISTING row scope that adopts a different row (an
|
|
635
|
+
* array shrink-then-regrow, a grown baseline row). Signals must be
|
|
636
|
+
* re-seeded in place, never replaced — computeds already wired to them
|
|
637
|
+
* keep tracking.
|
|
638
|
+
*/
|
|
639
|
+
reseedScope?(ctx: PluginCtx<TState>, scope: InternalObjectStore, raw: unknown): void;
|
|
640
|
+
/**
|
|
641
|
+
* 4 — per value leaf, from INSIDE reset's existing walk (scoped resets
|
|
642
|
+
* stay correct for free). Restore live plugin state to its decode-time
|
|
643
|
+
* baseline.
|
|
644
|
+
*/
|
|
645
|
+
resetField?(ctx: PluginCtx<TState>, store: InternalFieldStore): void;
|
|
646
|
+
/**
|
|
647
|
+
* 4b — live input write for a value leaf (`setFieldInput`). Return
|
|
648
|
+
* `true` when the plugin wrote `store.input` (and its own slot) so core
|
|
649
|
+
* does not write the leaf again. Envelope fields use this so a
|
|
650
|
+
* keystroke updates `envelope.value` through `writeEnvelope`.
|
|
651
|
+
*/
|
|
652
|
+
syncInput?(ctx: PluginCtx<TState>, store: InternalValueStore, input: unknown): boolean;
|
|
653
|
+
/**
|
|
654
|
+
* 4c — `reset({ initialInput })` has just written this leaf's
|
|
655
|
+
* `initialInput` from `raw`. Re-decode the start envelope from that
|
|
656
|
+
* same raw so `resetField` restores number and mode from one object.
|
|
657
|
+
* Not called from `applyBaseline` (rebase owns that adopt).
|
|
658
|
+
*/
|
|
659
|
+
syncInitial?(ctx: PluginCtx<TState>, store: InternalValueStore, raw: unknown): void;
|
|
660
|
+
/**
|
|
661
|
+
* 5 — after the value/baseline rebase of one object scope: the form root
|
|
662
|
+
* (from `applyBaseline`, with the raw decoded record) and each array-item
|
|
663
|
+
* object (from the baseline rebase walk, with the fresh raw row). The
|
|
664
|
+
* plugin owns its own decode of the raw.
|
|
665
|
+
*/
|
|
666
|
+
rebase?(ctx: PluginCtx<TState>, scope: InternalObjectStore, raw: unknown): void;
|
|
667
|
+
/**
|
|
668
|
+
* 6 — item-state transfer: per-field plugin state travels with its row
|
|
669
|
+
* through an insert, remove or move, exactly like the input signals
|
|
670
|
+
* (`copyItemState`). Field stores are position-fixed; only values move.
|
|
671
|
+
*/
|
|
672
|
+
transferField?(ctx: PluginCtx<TState>, from: InternalValueStore, to: InternalValueStore): void;
|
|
673
|
+
/**
|
|
674
|
+
* 7 — the swap twin of `transferField` (`swapItemState`).
|
|
675
|
+
*/
|
|
676
|
+
swapField?(ctx: PluginCtx<TState>, first: InternalValueStore, second: InternalValueStore): void;
|
|
677
|
+
/**
|
|
678
|
+
* 8 — whether THIS field's plugin state is dirty (must serialize). Feeds
|
|
679
|
+
* the subtree dirty walks that decide payload emission.
|
|
680
|
+
*/
|
|
681
|
+
fieldIsDirty?(ctx: PluginCtx<TState>, store: InternalValueStore): boolean;
|
|
682
|
+
/**
|
|
683
|
+
* 9 — wraps a value field's own payload entry (the LOS-573 envelope):
|
|
684
|
+
* called for a dirty leaf with its outgoing value (`undefined` when only
|
|
685
|
+
* the plugin half is dirty), returns the wire entry to emit — or
|
|
686
|
+
* `undefined` for "no opinion" (a bare scalar). Two plugins claiming the
|
|
687
|
+
* same field throw.
|
|
688
|
+
*/
|
|
689
|
+
encodeValue?(ctx: PluginCtx<TState>, store: InternalValueStore, valueOut: unknown): unknown;
|
|
690
|
+
/**
|
|
691
|
+
* 10 — the plugin's contribution to form-level `isDirty`. Runs inside a
|
|
692
|
+
* computed, so it MUST read its signals unconditionally — the aggregate
|
|
693
|
+
* never short-circuits between plugins (an unran handler contributes no
|
|
694
|
+
* signal reads and deafens the projection).
|
|
695
|
+
*/
|
|
696
|
+
isDirty?(ctx: PluginCtx<TState>): boolean;
|
|
697
|
+
/**
|
|
698
|
+
* 11 — members merged into the react field snapshot, computed inside the
|
|
699
|
+
* library-owned tracked read (the D5 snapshot model): plain values read
|
|
700
|
+
* off signals — never getters — plus identity-stable callbacks (a fresh
|
|
701
|
+
* closure per call would defeat the snapshot equality gate and re-render
|
|
702
|
+
* every notification; cache them on the plugin's per-field state). Types
|
|
703
|
+
* ride the `FieldStoreSlots` declare-module augmentation in the plugin's
|
|
704
|
+
* own file. A key claimed twice, or colliding with a core field member,
|
|
705
|
+
* throws.
|
|
706
|
+
*/
|
|
707
|
+
fieldSnapshot?(ctx: PluginCtx<TState>, store: InternalFieldStore, path: Path): Record<string, unknown>;
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* The `plugins` config entry: factories may be composed conditionally —
|
|
711
|
+
* falsy entries and nested arrays are accepted and flattened
|
|
712
|
+
* (`plugins: [envelopes(), engine && derivation(engine)]`).
|
|
713
|
+
*/
|
|
714
|
+
type PluginsInput = ReadonlyArray<JsonischPlugin<unknown> | false | null | undefined | ReadonlyArray<JsonischPlugin<unknown> | false | null | undefined>>;
|
|
715
|
+
//#endregion
|
|
716
|
+
//#region src/core/plugin/driver.d.ts
|
|
717
|
+
/**
|
|
718
|
+
* The hooks the driver precomputes implementer lists for (the per-field
|
|
719
|
+
* ones run in loops; scanning all plugins per field would be quadratic).
|
|
720
|
+
*/
|
|
721
|
+
declare const HOOK_NAMES: readonly ["buildScope", "reseedScope", "resetField", "syncInput", "syncInitial", "rebase", "transferField", "swapField", "fieldIsDirty", "encodeValue", "isDirty", "fieldSnapshot"];
|
|
722
|
+
type HookName = (typeof HOOK_NAMES)[number];
|
|
723
|
+
/**
|
|
724
|
+
* The resolved plugin runtime on a form store: the flat plugin list,
|
|
725
|
+
* per-hook implementer lists, and the envelope wire contracts keyed by
|
|
726
|
+
* control kind — all precomputed once at `createFormStore`.
|
|
727
|
+
*/
|
|
728
|
+
interface PluginDriver {
|
|
729
|
+
readonly plugins: readonly JsonischPlugin<unknown>[];
|
|
730
|
+
readonly hooks: Readonly<Record<HookName, readonly JsonischPlugin<unknown>[]>>;
|
|
731
|
+
/**
|
|
732
|
+
* Envelope wire contracts by control kind (`estimate` →
|
|
733
|
+
* `envelopesWire`), consulted by the leaf decode paths.
|
|
734
|
+
*/
|
|
735
|
+
readonly envelopes: ReadonlyMap<string, WireContract>;
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Whether any plugin's form-level state is dirty. Runs inside the
|
|
739
|
+
* `isDirty` aggregate computed, so it reads EVERY implementer — never
|
|
740
|
+
* short-circuits: an unran handler contributes no signal reads and would
|
|
741
|
+
* deafen the projection.
|
|
742
|
+
*/
|
|
743
|
+
declare function pluginsDirty(form: InternalFormStore): boolean;
|
|
744
|
+
/**
|
|
745
|
+
* Whether THIS value leaf's plugin state is dirty (must serialize). Reads
|
|
746
|
+
* every implementer for the same no-short-circuit reason as `pluginsDirty`
|
|
747
|
+
* — callers may sit inside computeds.
|
|
748
|
+
*/
|
|
749
|
+
declare function fieldPluginDirty(form: InternalFormStore, store: InternalValueStore): boolean;
|
|
750
|
+
/**
|
|
751
|
+
* Whether any value leaf in the subtree has dirty plugin state — the
|
|
752
|
+
* plugin half of the dirty walks that decide payload emission (a mode flip
|
|
753
|
+
* with an unchanged value must still produce a payload). Reads array
|
|
754
|
+
* `items` (not raw `children`, which may hold stale stores past the end
|
|
755
|
+
* after a shrink), so a reactive caller subscribes to structural changes.
|
|
756
|
+
*/
|
|
757
|
+
declare function hasPluginDirtyField(form: InternalFormStore, store: InternalFieldStore): boolean;
|
|
758
|
+
/**
|
|
759
|
+
* Encodes one dirty value leaf's payload entry: asks the `encodeValue`
|
|
760
|
+
* implementers to wrap it (the LOS-573 envelope). Exactly one plugin may
|
|
761
|
+
* claim a field; with no claim the outgoing value is emitted as-is.
|
|
762
|
+
*
|
|
763
|
+
* @param form The form store.
|
|
764
|
+
* @param store The value leaf being encoded.
|
|
765
|
+
* @param valueOut The outgoing value (`undefined` when only plugin state
|
|
766
|
+
* is dirty).
|
|
767
|
+
*
|
|
768
|
+
* @returns The wire entry to emit.
|
|
769
|
+
*/
|
|
770
|
+
declare function encodeFieldValue(form: InternalFormStore, store: InternalValueStore, valueOut: unknown): unknown;
|
|
771
|
+
/**
|
|
772
|
+
* Unwraps a raw persisted leaf entry through the form's envelope wire
|
|
773
|
+
* contracts: an envelope-control leaf resolves its value half, everything
|
|
774
|
+
* else passes through. The single decode seam shared by the walk, reset,
|
|
775
|
+
* and the baseline rebase — a leaf can never round-trip to two different
|
|
776
|
+
* values.
|
|
777
|
+
*/
|
|
778
|
+
declare function unwrapLeafInput(form: InternalFormStore, control: string, raw: unknown): unknown;
|
|
779
|
+
//#endregion
|
|
780
|
+
//#region src/core/types/form.d.ts
|
|
781
|
+
/**
|
|
782
|
+
* When validation runs. `validate` is the mode that arms validation the
|
|
783
|
+
* first time; `revalidate` takes over once the form is in the "already
|
|
784
|
+
* validated" state (submitted, or the triggering subtree has errors) — the
|
|
785
|
+
* formisch two-mode model: validate on submit, revalidate on input.
|
|
786
|
+
*/
|
|
787
|
+
type ValidationMode = "initial" | "touch" | "input" | "change" | "blur" | "submit";
|
|
788
|
+
/**
|
|
789
|
+
* One validation issue in the injected validator's output. Deliberately
|
|
790
|
+
* AJV-shaped (`ErrorObject` subset) so an app can pass a compiled AJV
|
|
791
|
+
* validate function's `errors` through unchanged — jsonisch itself never
|
|
792
|
+
* depends on AJV.
|
|
793
|
+
*/
|
|
794
|
+
interface ValidationIssue {
|
|
795
|
+
/**
|
|
796
|
+
* JSON-Pointer to the failing value (`""` for the root). Routed to the
|
|
797
|
+
* field store's `errors` signal; an unroutable pointer lands on the
|
|
798
|
+
* nearest addressable ancestor.
|
|
799
|
+
*/
|
|
800
|
+
readonly instancePath?: string | undefined;
|
|
801
|
+
/**
|
|
802
|
+
* The human-readable message. Falls back to a generic message when absent.
|
|
803
|
+
*/
|
|
804
|
+
readonly message?: string | undefined;
|
|
805
|
+
/**
|
|
806
|
+
* The failed JSON-Schema keyword. `required` issues point at the parent
|
|
807
|
+
* object; routing appends `params.missingProperty` so the error lands on
|
|
808
|
+
* the missing field itself.
|
|
809
|
+
*/
|
|
810
|
+
readonly keyword?: string | undefined;
|
|
811
|
+
/**
|
|
812
|
+
* Keyword-specific parameters (e.g. `missingProperty` for `required`).
|
|
813
|
+
*/
|
|
814
|
+
readonly params?: Record<string, unknown> | undefined;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* The injected validator: compiled ONCE per schema by the caller, returns
|
|
818
|
+
* the issues for an input (`null`/`undefined`/empty for a valid input).
|
|
819
|
+
* Synchronous by design — AJV is sync.
|
|
820
|
+
*/
|
|
821
|
+
type FormValidator = (input: unknown) => readonly ValidationIssue[] | null | undefined;
|
|
822
|
+
/**
|
|
823
|
+
* Configuration for creating a form store.
|
|
824
|
+
*/
|
|
825
|
+
interface FormConfig {
|
|
826
|
+
/**
|
|
827
|
+
* The JSON-Schema the form is derived from (runtime DB data).
|
|
828
|
+
*/
|
|
829
|
+
readonly schema: JsonSchema;
|
|
830
|
+
/**
|
|
831
|
+
* The initial input in the nested server-record shape (no pre-flattening).
|
|
832
|
+
* Keys not declared in the schema never enter form state (the schema is
|
|
833
|
+
* the allow-list).
|
|
834
|
+
*/
|
|
835
|
+
readonly initialInput?: unknown;
|
|
836
|
+
/**
|
|
837
|
+
* Read-only eval scope for formula resolution (off-form values). Becomes a
|
|
838
|
+
* settable signal on the store; `applyBaseline` updates it.
|
|
839
|
+
*/
|
|
840
|
+
readonly offFormValues?: Record<string, unknown>;
|
|
841
|
+
/**
|
|
842
|
+
* The identifier the host's stored formulas use for the root record
|
|
843
|
+
* (`record` by default). Why it exists: a row's eval scope is sealed to
|
|
844
|
+
* its own columns, so a per-row calc that needs a root value — an
|
|
845
|
+
* asset's share of `loan[totalLoanAmount]` — can only reach the root
|
|
846
|
+
* through this one name. Reserved in row scope (it wins over a row
|
|
847
|
+
* column of the same name); hosts whose schemas already say `loan[…]`
|
|
848
|
+
* pass `"loan"`.
|
|
849
|
+
*/
|
|
850
|
+
readonly rootRecordAlias?: string | undefined;
|
|
851
|
+
/**
|
|
852
|
+
* The registered plugins, run in array order within each hook. Falsy
|
|
853
|
+
* entries and one level of nesting are accepted
|
|
854
|
+
* (`plugins: [envelopes(), engine && derivation(engine)]`). Everything
|
|
855
|
+
* computed on top of the base pipeline — envelope meta state,
|
|
856
|
+
* derivation, visibility — registers here; a form without plugins is a
|
|
857
|
+
* plain schema-walked value store.
|
|
858
|
+
*/
|
|
859
|
+
readonly plugins?: PluginsInput | undefined;
|
|
860
|
+
/**
|
|
861
|
+
* The empty input a required field without an initial input starts at,
|
|
862
|
+
* keyed by JSON-Schema type. Merged over the default (`{ string: "" }` —
|
|
863
|
+
* required strings start as `""`, every other type as `undefined`).
|
|
864
|
+
*/
|
|
865
|
+
readonly emptyInput?: Record<string, unknown>;
|
|
866
|
+
/**
|
|
867
|
+
* The injected validator, compiled once per schema by the caller. Without
|
|
868
|
+
* one the form always validates successfully (enforcement rollout is
|
|
869
|
+
* per-form opt-in).
|
|
870
|
+
*/
|
|
871
|
+
readonly validator?: FormValidator | undefined;
|
|
872
|
+
/**
|
|
873
|
+
* The validation mode of the form. Defaults to `"submit"`.
|
|
874
|
+
*/
|
|
875
|
+
readonly validate?: ValidationMode | undefined;
|
|
876
|
+
/**
|
|
877
|
+
* The revalidation mode of the form. Defaults to `"input"`.
|
|
878
|
+
*/
|
|
879
|
+
readonly revalidate?: Exclude<ValidationMode, "initial"> | undefined;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* The `computed`-cached form-level aggregates. Each one wraps a whole-tree
|
|
883
|
+
* `getFieldBool` walk (plus the meta channel for `isDirty`) so the walk
|
|
884
|
+
* runs once per invalidation instead of once per read — under the react
|
|
885
|
+
* adapter's snapshot model `useForm` reads these on every notification,
|
|
886
|
+
* which without caching would mean four full-tree walks per keystroke.
|
|
887
|
+
*/
|
|
888
|
+
interface FormAggregates {
|
|
889
|
+
/**
|
|
890
|
+
* Whether any field in the form has been touched.
|
|
891
|
+
*/
|
|
892
|
+
readonly isTouched: ReadonlySignal<boolean>;
|
|
893
|
+
/**
|
|
894
|
+
* Whether any field in the form has been edited.
|
|
895
|
+
*/
|
|
896
|
+
readonly isEdited: ReadonlySignal<boolean>;
|
|
897
|
+
/**
|
|
898
|
+
* Whether any field differs from its start input, OR any root-level
|
|
899
|
+
* field's meta channel is dirty (a mode flip with an unchanged value
|
|
900
|
+
* still produces a payload, so Save must enable).
|
|
901
|
+
*/
|
|
902
|
+
readonly isDirty: ReadonlySignal<boolean>;
|
|
903
|
+
/**
|
|
904
|
+
* Whether no field in the form has validation errors. Calc errors are
|
|
905
|
+
* excluded — only user-fixable validation gates validity.
|
|
906
|
+
*/
|
|
907
|
+
readonly isValid: ReadonlySignal<boolean>;
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* The internal form store: the root object node plus form-level state.
|
|
911
|
+
*/
|
|
912
|
+
interface InternalFormStore extends InternalObjectStore {
|
|
913
|
+
/**
|
|
914
|
+
* The resolved empty-input config (defaults merged with the form config),
|
|
915
|
+
* read by the walk when defaulting required fields without initial input.
|
|
916
|
+
*/
|
|
917
|
+
emptyInput: Record<string, unknown>;
|
|
918
|
+
/**
|
|
919
|
+
* The resolved root-record alias (config value or the default) —
|
|
920
|
+
* the one home both the shelf alias writer and the scope resolvers read.
|
|
921
|
+
*/
|
|
922
|
+
rootRecordAlias: string;
|
|
923
|
+
/**
|
|
924
|
+
* The injected validator, or `undefined` for a form without enforcement.
|
|
925
|
+
*/
|
|
926
|
+
validator: FormValidator | undefined;
|
|
927
|
+
/**
|
|
928
|
+
* The resolved plugin runtime: flat plugin list, per-hook implementer
|
|
929
|
+
* lists, envelope wire contracts by control kind. Set BEFORE the walk so
|
|
930
|
+
* the walk can dispatch scope hooks for the rows it creates.
|
|
931
|
+
*/
|
|
932
|
+
pluginDriver: PluginDriver;
|
|
933
|
+
/**
|
|
934
|
+
* Each plugin's state container, keyed by its `PluginKey` identity
|
|
935
|
+
* (created by the plugin's `build`). Read through the exported keys
|
|
936
|
+
* (`envelopesKey.get(form, store)`), never directly.
|
|
937
|
+
*/
|
|
938
|
+
pluginState: Map<PluginKey<unknown>, unknown>;
|
|
939
|
+
/**
|
|
940
|
+
* The validation mode of the form.
|
|
941
|
+
*/
|
|
942
|
+
validate: ValidationMode;
|
|
943
|
+
/**
|
|
944
|
+
* The revalidation mode of the form.
|
|
945
|
+
*/
|
|
946
|
+
revalidate: Exclude<ValidationMode, "initial">;
|
|
947
|
+
/**
|
|
948
|
+
* The number of active validators (kept as a counter so a future async
|
|
949
|
+
* validator cannot flicker `isValidating`).
|
|
950
|
+
*/
|
|
951
|
+
validators: number;
|
|
952
|
+
/**
|
|
953
|
+
* The form element (react adapter only; unset on the server).
|
|
954
|
+
*/
|
|
955
|
+
element?: HTMLFormElement | undefined;
|
|
956
|
+
/**
|
|
957
|
+
* Off-form values used to fill `undefined` in formula scope resolution.
|
|
958
|
+
* Settable: `applyBaseline` updates it and dependents re-resolve.
|
|
959
|
+
*/
|
|
960
|
+
offFormValues: Signal<Record<string, unknown>>;
|
|
961
|
+
/**
|
|
962
|
+
* The submitting state of the form.
|
|
963
|
+
*/
|
|
964
|
+
isSubmitting: Signal<boolean>;
|
|
965
|
+
/**
|
|
966
|
+
* The submitted state of the form.
|
|
967
|
+
*/
|
|
968
|
+
isSubmitted: Signal<boolean>;
|
|
969
|
+
/**
|
|
970
|
+
* The validating state of the form.
|
|
971
|
+
*/
|
|
972
|
+
isValidating: Signal<boolean>;
|
|
973
|
+
/**
|
|
974
|
+
* The cached form-level aggregates (see `FormAggregates`).
|
|
975
|
+
*/
|
|
976
|
+
aggregates: FormAggregates;
|
|
977
|
+
}
|
|
978
|
+
//#endregion
|
|
979
|
+
//#region src/methods/form-ref.d.ts
|
|
980
|
+
/**
|
|
981
|
+
* What every method accepts as its form argument: the internal form store
|
|
982
|
+
* itself, or any wrapper exposing it as `internal` (the react adapter's
|
|
983
|
+
* public `FormStore`). App code passes the wrapper; core and tests pass the
|
|
984
|
+
* internal store directly.
|
|
985
|
+
*/
|
|
986
|
+
type FormRef = InternalFormStore | {
|
|
987
|
+
readonly internal: InternalFormStore;
|
|
988
|
+
};
|
|
989
|
+
/**
|
|
990
|
+
* Unwraps a `FormRef` to the internal form store.
|
|
991
|
+
*/
|
|
992
|
+
declare function internalOf(form: FormRef): InternalFormStore;
|
|
993
|
+
//#endregion
|
|
994
|
+
export { VisibleWhen as A, createSignal as B, FieldErrors as C, InternalFieldStore as D, InternalBaseStore as E, ReadonlySignal as F, JsonSchema as G, getListener as H, Signal as I, Path as K, Tracker as L, ControlKind as M, inferControl as N, InternalObjectStore as O, Listener as P, batch as R, FieldElement as S, InternalArrayStore as T, untrack as U, createTracker as V, withListener as W, WireContract as _, InternalFormStore as a, PluginKey as b, PluginDriver as c, hasPluginDirtyField as d, pluginsDirty as f, PluginsInput as g, PluginCtx as h, FormValidator as i, VisibleWhenOp as j, InternalValueStore as k, encodeFieldValue as l, JsonischPlugin as m, internalOf as n, ValidationIssue as o, unwrapLeafInput as p, PathSegment as q, FormConfig as r, ValidationMode as s, FormRef as t, fieldPluginDirty as u, WireEnvelope as v, FieldKind as w, ContainerInput as x, FieldSlotKey as y, computed as z };
|