@formwright/core 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/LICENSE +21 -0
- package/dist/chunk-EZUHEI5F.js +162 -0
- package/dist/chunk-EZUHEI5F.js.map +1 -0
- package/dist/index.cjs +764 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +259 -0
- package/dist/index.d.ts +259 -0
- package/dist/index.js +585 -0
- package/dist/index.js.map +1 -0
- package/dist/reactive.cjs +169 -0
- package/dist/reactive.cjs.map +1 -0
- package/dist/reactive.d.cts +45 -0
- package/dist/reactive.d.ts +45 -0
- package/dist/reactive.js +3 -0
- package/dist/reactive.js.map +1 -0
- package/package.json +46 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { ReadSignal, WriteSignal, Dispose } from './reactive.js';
|
|
2
|
+
export { batch, computed, effect, isTracking, signal, untrack } from './reactive.js';
|
|
3
|
+
import { FieldValue, Condition, ValidationSchema, ProviderRef, FieldOption, Resolvable, FieldSchema, FormSchema } from '@formwright/schema';
|
|
4
|
+
export { Condition, FieldOption, FieldSchema, FieldValue, FormSchema } from '@formwright/schema';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Condition engine — evaluates the sandboxed JSONLogic-style {@link Condition}
|
|
8
|
+
* algebra from a schema against the current form values.
|
|
9
|
+
*
|
|
10
|
+
* It is pure and synchronous: `getValue` reads a field. When the evaluator is
|
|
11
|
+
* called inside an {@link effect}/{@link computed} and `getValue` reads field
|
|
12
|
+
* signals, the result automatically tracks *only* the fields the condition
|
|
13
|
+
* references — so a `visibleWhen` re-evaluates exactly when its inputs change.
|
|
14
|
+
* Conditions are data, never `eval`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
type ValueGetter = (fieldId: string) => FieldValue;
|
|
18
|
+
/** Evaluate a condition to a boolean. `undefined` conditions default to `true`. */
|
|
19
|
+
declare function evaluateCondition(cond: Condition | undefined, get: ValueGetter, fallback?: boolean): boolean;
|
|
20
|
+
/** Collect the field ids referenced by a condition (for documentation / codegen). */
|
|
21
|
+
declare function referencedFields(cond: Condition | undefined): string[];
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Field-level validation — turns a declarative {@link ValidationSchema} into a
|
|
25
|
+
* pure validator `(value) => error | null`.
|
|
26
|
+
*
|
|
27
|
+
* This covers the zero-config built-ins. For richer rules, a field can instead
|
|
28
|
+
* carry any Standard-Schema-compatible validator (Zod/Valibot/ArkType); the Form
|
|
29
|
+
* runs whichever is present. Kept dependency-free here so `@formwright/core`
|
|
30
|
+
* ships with no required runtime deps.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
type FieldValidator = (value: FieldValue) => string | null;
|
|
34
|
+
/** Compile a declarative validation descriptor into a validator function. */
|
|
35
|
+
declare function compileValidator(schema: ValidationSchema): FieldValidator;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Provider system — the host app injects integrations (i18n, data fetching,
|
|
39
|
+
* theming) that the schema references via sigils (`{ $t }`, `{ $query }`,
|
|
40
|
+
* `{ $theme }`). Providers expose plain functions (optionally signal-backed),
|
|
41
|
+
* so when a locale changes or a query resolves, only bound nodes update.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
interface I18nProvider {
|
|
45
|
+
/** Translate a key with optional interpolation args. */
|
|
46
|
+
t(key: string, args?: Record<string, FieldValue>): string;
|
|
47
|
+
}
|
|
48
|
+
interface QueryResult<T> {
|
|
49
|
+
readonly data: T | undefined;
|
|
50
|
+
readonly loading: boolean;
|
|
51
|
+
readonly error: unknown;
|
|
52
|
+
}
|
|
53
|
+
interface QueryProvider {
|
|
54
|
+
/** Return a reactive query result for a key and optional params. */
|
|
55
|
+
query<T = unknown>(key: string, params?: Record<string, unknown>): ReadSignal<QueryResult<T>>;
|
|
56
|
+
}
|
|
57
|
+
interface ThemeProvider {
|
|
58
|
+
token(name: string): string;
|
|
59
|
+
}
|
|
60
|
+
interface Providers {
|
|
61
|
+
readonly i18n?: I18nProvider;
|
|
62
|
+
readonly query?: QueryProvider;
|
|
63
|
+
readonly theme?: ThemeProvider;
|
|
64
|
+
readonly [name: string]: unknown;
|
|
65
|
+
}
|
|
66
|
+
declare function isProviderRef(value: unknown): value is ProviderRef;
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a {@link Resolvable} to a concrete value using the available providers.
|
|
69
|
+
* Literals pass through; sigils dispatch to the matching provider. Unresolved
|
|
70
|
+
* refs fall back to a readable string so the form still renders.
|
|
71
|
+
*/
|
|
72
|
+
declare function resolve<T extends FieldValue | readonly FieldOption[]>(value: Resolvable<T> | undefined, providers: Providers | undefined): T | undefined;
|
|
73
|
+
/** Resolve a `$query` ref to its reactive result signal, or `null` if not a query ref. */
|
|
74
|
+
declare function resolveQuery(value: unknown, providers: Providers | undefined): ReadSignal<QueryResult<unknown>> | null;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Reactive per-field model. Each {@link FieldState} owns the signals for one
|
|
78
|
+
* field — its value, error, and touched flag — plus computed `visible`,
|
|
79
|
+
* `enabled`, and `required` derived from the schema's conditions. The renderer
|
|
80
|
+
* binds directly to these signals, so a change updates only the affected nodes.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** Default value for a leaf field with no explicit initial/default. */
|
|
84
|
+
declare function defaultValueFor(type: string): FieldValue;
|
|
85
|
+
declare class FieldState {
|
|
86
|
+
/** Discriminant for the {@link FieldNode} union (leaf vs group/collection). */
|
|
87
|
+
readonly kind: "field";
|
|
88
|
+
readonly id: string;
|
|
89
|
+
readonly schema: FieldSchema;
|
|
90
|
+
readonly value: WriteSignal<FieldValue>;
|
|
91
|
+
readonly error: WriteSignal<string | null>;
|
|
92
|
+
readonly touched: WriteSignal<boolean>;
|
|
93
|
+
readonly visible: ReadSignal<boolean>;
|
|
94
|
+
readonly enabled: ReadSignal<boolean>;
|
|
95
|
+
readonly required: ReadSignal<boolean>;
|
|
96
|
+
private readonly validator;
|
|
97
|
+
constructor(schema: FieldSchema, initial: FieldValue, getValue: ValueGetter);
|
|
98
|
+
/** Run validation, store and return the error (or null). Hidden fields never error. */
|
|
99
|
+
validate(): string | null;
|
|
100
|
+
reset(value: FieldValue): void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Nested field tree — `group` (object) and `collection` (array-of-groups) nodes
|
|
105
|
+
* on top of the leaf {@link FieldState}.
|
|
106
|
+
*
|
|
107
|
+
* Names in conditions resolve **lexically**: a child first looks among its own
|
|
108
|
+
* siblings, then walks up the enclosing {@link Scope} chain to the form root. So
|
|
109
|
+
* a field nested inside a group or a collection row can be hidden by an outer
|
|
110
|
+
* toggle (`{ var: "showDetails" }`) *and* by a sibling in the same row — without
|
|
111
|
+
* any path syntax. Hidden fields keep their value in the aggregated payload but
|
|
112
|
+
* are skipped by validation.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
/** A node in the field tree: a leaf field, a nested object, or a repeatable list. */
|
|
116
|
+
type FieldNode = FieldState | GroupNode | CollectionNode;
|
|
117
|
+
/** Resolves a referenced field name to its current value, walking enclosing scopes. */
|
|
118
|
+
type Scope = ValueGetter;
|
|
119
|
+
type Dict = Record<string, unknown>;
|
|
120
|
+
/** A nested object field: produces `{ ...visible child values }`. */
|
|
121
|
+
declare class GroupNode {
|
|
122
|
+
readonly kind: "group";
|
|
123
|
+
readonly id: string;
|
|
124
|
+
readonly schema: FieldSchema;
|
|
125
|
+
readonly children: readonly FieldNode[];
|
|
126
|
+
readonly byName: ReadonlyMap<string, FieldNode>;
|
|
127
|
+
readonly value: ReadSignal<Dict>;
|
|
128
|
+
readonly visible: ReadSignal<boolean>;
|
|
129
|
+
readonly enabled: ReadSignal<boolean>;
|
|
130
|
+
/** The scope a child uses: resolve a name among siblings, else delegate upward. */
|
|
131
|
+
readonly scope: Scope;
|
|
132
|
+
constructor(schema: FieldSchema, parentScope: Scope, initial: Dict);
|
|
133
|
+
reset(initial: Dict): void;
|
|
134
|
+
}
|
|
135
|
+
/** One row of a {@link CollectionNode}: a group with a stable identity key. */
|
|
136
|
+
interface CollectionItem {
|
|
137
|
+
readonly key: number;
|
|
138
|
+
readonly group: GroupNode;
|
|
139
|
+
}
|
|
140
|
+
/** A repeatable list of object rows: produces `[{ ... }, { ... }]`. */
|
|
141
|
+
declare class CollectionNode {
|
|
142
|
+
readonly kind: "collection";
|
|
143
|
+
readonly id: string;
|
|
144
|
+
readonly schema: FieldSchema;
|
|
145
|
+
readonly value: ReadSignal<Dict[]>;
|
|
146
|
+
readonly visible: ReadSignal<boolean>;
|
|
147
|
+
readonly enabled: ReadSignal<boolean>;
|
|
148
|
+
private readonly rows;
|
|
149
|
+
private readonly parentScope;
|
|
150
|
+
private readonly itemSchema;
|
|
151
|
+
private seq;
|
|
152
|
+
constructor(schema: FieldSchema, parentScope: Scope, initial: Dict[]);
|
|
153
|
+
/** Reactive list of rows (subscribes the caller to add/remove). */
|
|
154
|
+
get items(): ReadSignal<CollectionItem[]>;
|
|
155
|
+
private makeItem;
|
|
156
|
+
private seedRows;
|
|
157
|
+
/** Append a new empty row, unless `maxItems` is reached. */
|
|
158
|
+
add(): void;
|
|
159
|
+
/** Remove the row at `index`, unless `minItems` would be violated. */
|
|
160
|
+
removeAt(index: number): void;
|
|
161
|
+
reset(initial: Dict[]): void;
|
|
162
|
+
}
|
|
163
|
+
/** Build the top-level field tree for a form, rooted at `rootScope`. */
|
|
164
|
+
declare function buildTree(schemas: readonly FieldSchema[], initial: Dict): {
|
|
165
|
+
nodes: FieldNode[];
|
|
166
|
+
byName: Map<string, FieldNode>;
|
|
167
|
+
scope: Scope;
|
|
168
|
+
};
|
|
169
|
+
/** Visit every leaf {@link FieldState} in a node list (descends groups/collections). */
|
|
170
|
+
declare function eachLeaf(nodes: readonly FieldNode[], visit: (leaf: FieldState) => void): void;
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* The {@link Form} class — Formwright's public, class-based, imperative API.
|
|
174
|
+
*
|
|
175
|
+
* const form = new Form(schema, { email: "" }, options);
|
|
176
|
+
* form.mount(document.getElementById("root")!);
|
|
177
|
+
* await form.submit();
|
|
178
|
+
*
|
|
179
|
+
* The instance owns the reactive field graph, validation, the condition engine,
|
|
180
|
+
* provider wiring, and the submission pipeline. It is render-agnostic: `mount`
|
|
181
|
+
* delegates to a registered renderer (e.g. `@formwright/dom`), so the same
|
|
182
|
+
* instance can drive the DOM, a web component, or a framework adapter.
|
|
183
|
+
*/
|
|
184
|
+
|
|
185
|
+
/** Form values — nested for `group` (object) and `collection` (array) fields. */
|
|
186
|
+
type FormValues = Record<string, unknown>;
|
|
187
|
+
type FormErrors = Record<string, string | null>;
|
|
188
|
+
/** A transform applied to values before submission. */
|
|
189
|
+
type Transform = (values: FormValues, form: Form) => unknown;
|
|
190
|
+
/** Handlers referenced by name from the schema's `submit` block. */
|
|
191
|
+
type SuccessHandler = (result: unknown, form: Form) => void;
|
|
192
|
+
type ErrorHandler = (error: unknown, form: Form) => void;
|
|
193
|
+
interface FormOptions {
|
|
194
|
+
readonly providers?: Providers;
|
|
195
|
+
readonly transforms?: Record<string, Transform>;
|
|
196
|
+
readonly handlers?: Record<string, SuccessHandler | ErrorHandler>;
|
|
197
|
+
/** Override the network send (defaults to `fetch` against `submit.endpoint`). */
|
|
198
|
+
readonly send?: (payload: unknown, form: Form) => Promise<unknown>;
|
|
199
|
+
}
|
|
200
|
+
/** Renders a {@link Form} into a host node; returns a disposer. Provided by a renderer package. */
|
|
201
|
+
interface FormRenderer {
|
|
202
|
+
mount(form: Form, host: Element): Dispose;
|
|
203
|
+
}
|
|
204
|
+
type EventName = "submit" | "success" | "error" | "change";
|
|
205
|
+
type Listener = (payload: unknown) => void;
|
|
206
|
+
/** Register the renderer used by {@link Form.mount} when none is passed explicitly. */
|
|
207
|
+
declare function setDefaultRenderer(renderer: FormRenderer): void;
|
|
208
|
+
declare class Form {
|
|
209
|
+
readonly schema: FormSchema;
|
|
210
|
+
readonly options: FormOptions;
|
|
211
|
+
/** Top-level field tree (leaf fields, groups, and collections, in order). */
|
|
212
|
+
readonly tree: readonly FieldNode[];
|
|
213
|
+
readonly order: readonly string[];
|
|
214
|
+
/** Reactive snapshot of all field values (nested for groups/collections). */
|
|
215
|
+
readonly values: ReadSignal<FormValues>;
|
|
216
|
+
/** True when the current values differ from the initial values. */
|
|
217
|
+
readonly isDirty: ReadSignal<boolean>;
|
|
218
|
+
/** True when no visible field currently has an error. */
|
|
219
|
+
readonly isValid: ReadSignal<boolean>;
|
|
220
|
+
private readonly submitting;
|
|
221
|
+
private readonly initialValues;
|
|
222
|
+
private readonly initialSnapshot;
|
|
223
|
+
private readonly rootByName;
|
|
224
|
+
private readonly listeners;
|
|
225
|
+
private disposeRenderer;
|
|
226
|
+
constructor(schema: FormSchema | unknown, initialValues?: FormValues, options?: FormOptions);
|
|
227
|
+
/** All leaf fields keyed by dotted path (e.g. `items.name`, `contacts.0.email`). */
|
|
228
|
+
get fields(): ReadonlyMap<string, FieldState>;
|
|
229
|
+
/** Resolve a leaf field by dotted path. Top-level ids work directly. */
|
|
230
|
+
field(path: string): FieldState | undefined;
|
|
231
|
+
getValue(path: string): FieldValue;
|
|
232
|
+
setValue(path: string, value: FieldValue): void;
|
|
233
|
+
/** Apply a value to a specific leaf node (used by the renderer). */
|
|
234
|
+
setFieldValue(field: FieldState, value: FieldValue): void;
|
|
235
|
+
setError(id: string, error: string | null): void;
|
|
236
|
+
setErrors(errors: FormErrors): void;
|
|
237
|
+
get isSubmitting(): ReadSignal<boolean>;
|
|
238
|
+
/** Validate every (visible) leaf field; returns true when the whole form is valid. */
|
|
239
|
+
validate(): boolean;
|
|
240
|
+
/** Run the submission pipeline: validate → transform → send → onSuccess/onError. */
|
|
241
|
+
submit(): Promise<unknown>;
|
|
242
|
+
reset(values?: FormValues): void;
|
|
243
|
+
/** Mount into a host element using the given renderer (or the registered default). */
|
|
244
|
+
mount(host: Element, renderer?: FormRenderer | null): Dispose;
|
|
245
|
+
destroy(): void;
|
|
246
|
+
on(event: EventName, listener: Listener): Dispose;
|
|
247
|
+
private emit;
|
|
248
|
+
private collectErrors;
|
|
249
|
+
private applyTransform;
|
|
250
|
+
private send;
|
|
251
|
+
private runSuccessHandler;
|
|
252
|
+
private runErrorHandler;
|
|
253
|
+
}
|
|
254
|
+
declare class FormValidationError extends Error {
|
|
255
|
+
readonly errors: FormErrors;
|
|
256
|
+
constructor(errors: FormErrors);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export { type CollectionItem, CollectionNode, Dispose, type ErrorHandler, type FieldNode, FieldState, type FieldValidator, Form, type FormErrors, type FormOptions, type FormRenderer, FormValidationError, type FormValues, GroupNode, type I18nProvider, type Providers, type QueryProvider, type QueryResult, ReadSignal, type Scope, type SuccessHandler, type ThemeProvider, type Transform, type ValueGetter, WriteSignal, buildTree, compileValidator, defaultValueFor, eachLeaf, evaluateCondition, isProviderRef, referencedFields, resolve, resolveQuery, setDefaultRenderer };
|