@formwright/core 0.1.0 → 0.2.2
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/README.md +96 -0
- package/dist/chunk-OWEG6VGP.js +3 -0
- package/dist/chunk-OWEG6VGP.js.map +1 -0
- package/dist/index.cjs +376 -232
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +105 -17
- package/dist/index.d.ts +105 -17
- package/dist/index.js +330 -51
- package/dist/index.js.map +1 -1
- package/dist/reactive.cjs +27 -164
- package/dist/reactive.cjs.map +1 -1
- package/dist/reactive.d.cts +1 -45
- package/dist/reactive.d.ts +1 -45
- package/dist/reactive.js +1 -1
- package/package.json +3 -2
- package/dist/chunk-EZUHEI5F.js +0 -162
- package/dist/chunk-EZUHEI5F.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,165 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var reactive = require('@formwright/reactive');
|
|
3
4
|
var schema = require('@formwright/schema');
|
|
4
5
|
|
|
5
6
|
// src/reactive.ts
|
|
6
|
-
var activeObserver = null;
|
|
7
|
-
var batchDepth = 0;
|
|
8
|
-
var pendingEffects = /* @__PURE__ */ new Set();
|
|
9
|
-
var flushing = false;
|
|
10
|
-
function link(source) {
|
|
11
|
-
const obs = activeObserver;
|
|
12
|
-
if (obs === null) return;
|
|
13
|
-
if (!source.observers.has(obs)) {
|
|
14
|
-
source.observers.add(obs);
|
|
15
|
-
obs.sources.add(source);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
function clearSources(obs) {
|
|
19
|
-
for (const src of obs.sources) src.observers.delete(obs);
|
|
20
|
-
obs.sources.clear();
|
|
21
|
-
}
|
|
22
|
-
function flush() {
|
|
23
|
-
if (flushing) return;
|
|
24
|
-
flushing = true;
|
|
25
|
-
try {
|
|
26
|
-
while (pendingEffects.size > 0) {
|
|
27
|
-
const next = pendingEffects.values().next().value;
|
|
28
|
-
pendingEffects.delete(next);
|
|
29
|
-
next.run();
|
|
30
|
-
}
|
|
31
|
-
} finally {
|
|
32
|
-
flushing = false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
var SignalNode = class {
|
|
36
|
-
constructor(value) {
|
|
37
|
-
this.value = value;
|
|
38
|
-
}
|
|
39
|
-
value;
|
|
40
|
-
observers = /* @__PURE__ */ new Set();
|
|
41
|
-
get() {
|
|
42
|
-
link(this);
|
|
43
|
-
return this.value;
|
|
44
|
-
}
|
|
45
|
-
peek() {
|
|
46
|
-
return this.value;
|
|
47
|
-
}
|
|
48
|
-
set(next) {
|
|
49
|
-
if (Object.is(next, this.value)) return;
|
|
50
|
-
this.value = next;
|
|
51
|
-
for (const obs of [...this.observers]) obs.notify();
|
|
52
|
-
if (batchDepth === 0) flush();
|
|
53
|
-
}
|
|
54
|
-
update(fn) {
|
|
55
|
-
this.set(fn(this.value));
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
var ComputedNode = class {
|
|
59
|
-
constructor(fn) {
|
|
60
|
-
this.fn = fn;
|
|
61
|
-
}
|
|
62
|
-
fn;
|
|
63
|
-
observers = /* @__PURE__ */ new Set();
|
|
64
|
-
sources = /* @__PURE__ */ new Set();
|
|
65
|
-
value;
|
|
66
|
-
dirty = true;
|
|
67
|
-
notify() {
|
|
68
|
-
if (this.dirty) return;
|
|
69
|
-
this.dirty = true;
|
|
70
|
-
for (const obs of [...this.observers]) obs.notify();
|
|
71
|
-
}
|
|
72
|
-
get() {
|
|
73
|
-
link(this);
|
|
74
|
-
if (this.dirty) this.recompute();
|
|
75
|
-
return this.value;
|
|
76
|
-
}
|
|
77
|
-
peek() {
|
|
78
|
-
if (this.dirty) this.recompute();
|
|
79
|
-
return this.value;
|
|
80
|
-
}
|
|
81
|
-
recompute() {
|
|
82
|
-
clearSources(this);
|
|
83
|
-
const prev = activeObserver;
|
|
84
|
-
activeObserver = this;
|
|
85
|
-
try {
|
|
86
|
-
this.value = this.fn();
|
|
87
|
-
this.dirty = false;
|
|
88
|
-
} finally {
|
|
89
|
-
activeObserver = prev;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
|
-
var EffectNode = class {
|
|
94
|
-
constructor(fn) {
|
|
95
|
-
this.fn = fn;
|
|
96
|
-
this.run();
|
|
97
|
-
}
|
|
98
|
-
fn;
|
|
99
|
-
sources = /* @__PURE__ */ new Set();
|
|
100
|
-
cleanup = void 0;
|
|
101
|
-
disposed = false;
|
|
102
|
-
notify() {
|
|
103
|
-
if (this.disposed) return;
|
|
104
|
-
pendingEffects.add(this);
|
|
105
|
-
}
|
|
106
|
-
run() {
|
|
107
|
-
if (this.disposed) return;
|
|
108
|
-
this.runCleanup();
|
|
109
|
-
clearSources(this);
|
|
110
|
-
const prev = activeObserver;
|
|
111
|
-
activeObserver = this;
|
|
112
|
-
try {
|
|
113
|
-
this.cleanup = this.fn();
|
|
114
|
-
} finally {
|
|
115
|
-
activeObserver = prev;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
runCleanup() {
|
|
119
|
-
if (typeof this.cleanup === "function") {
|
|
120
|
-
this.cleanup();
|
|
121
|
-
this.cleanup = void 0;
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
dispose() {
|
|
125
|
-
if (this.disposed) return;
|
|
126
|
-
this.disposed = true;
|
|
127
|
-
this.runCleanup();
|
|
128
|
-
clearSources(this);
|
|
129
|
-
pendingEffects.delete(this);
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
function signal(initial) {
|
|
133
|
-
return new SignalNode(initial);
|
|
134
|
-
}
|
|
135
|
-
function computed(fn) {
|
|
136
|
-
return new ComputedNode(fn);
|
|
137
|
-
}
|
|
138
|
-
function effect(fn) {
|
|
139
|
-
const node = new EffectNode(fn);
|
|
140
|
-
return () => node.dispose();
|
|
141
|
-
}
|
|
142
|
-
function untrack(fn) {
|
|
143
|
-
const prev = activeObserver;
|
|
144
|
-
activeObserver = null;
|
|
145
|
-
try {
|
|
146
|
-
return fn();
|
|
147
|
-
} finally {
|
|
148
|
-
activeObserver = prev;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
function batch(fn) {
|
|
152
|
-
batchDepth++;
|
|
153
|
-
try {
|
|
154
|
-
return fn();
|
|
155
|
-
} finally {
|
|
156
|
-
batchDepth--;
|
|
157
|
-
if (batchDepth === 0) flush();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function isTracking() {
|
|
161
|
-
return activeObserver !== null;
|
|
162
|
-
}
|
|
163
7
|
|
|
164
8
|
// src/conditions.ts
|
|
165
9
|
function isOp(cond, key) {
|
|
@@ -249,35 +93,42 @@ function isEmpty(value) {
|
|
|
249
93
|
return value === void 0 || value === null || value === "";
|
|
250
94
|
}
|
|
251
95
|
function compileValidator(schema) {
|
|
96
|
+
const overrides = schema.messages ?? {};
|
|
97
|
+
const msg = (rule, fallback) => overrides[rule] ?? (typeof schema.message === "string" ? schema.message : fallback);
|
|
252
98
|
return (value) => {
|
|
253
|
-
const msg = (fallback) => typeof schema.message === "string" ? schema.message : fallback;
|
|
254
99
|
if (isEmpty(value)) {
|
|
255
|
-
return schema.required ? msg("This field is required") : null;
|
|
100
|
+
return schema.required ? msg("required", "This field is required") : null;
|
|
256
101
|
}
|
|
257
102
|
if (schema.kind === "string" || schema.format) {
|
|
258
103
|
const str = String(value);
|
|
259
104
|
if (schema.minLength !== void 0 && str.length < schema.minLength) {
|
|
260
|
-
return msg(`Must be at least ${schema.minLength} characters`);
|
|
105
|
+
return msg("minLength", `Must be at least ${schema.minLength} characters`);
|
|
261
106
|
}
|
|
262
107
|
if (schema.maxLength !== void 0 && str.length > schema.maxLength) {
|
|
263
|
-
return msg(`Must be at most ${schema.maxLength} characters`);
|
|
108
|
+
return msg("maxLength", `Must be at most ${schema.maxLength} characters`);
|
|
264
109
|
}
|
|
265
110
|
if (schema.pattern !== void 0 && !new RegExp(schema.pattern).test(str)) {
|
|
266
|
-
return msg("Invalid format");
|
|
111
|
+
return msg("pattern", "Invalid format");
|
|
267
112
|
}
|
|
268
|
-
if (schema.format === "email" && !EMAIL.test(str))
|
|
269
|
-
|
|
270
|
-
if (schema.format === "
|
|
113
|
+
if (schema.format === "email" && !EMAIL.test(str))
|
|
114
|
+
return msg("format", "Enter a valid email");
|
|
115
|
+
if (schema.format === "url" && !URL.test(str)) return msg("format", "Enter a valid URL");
|
|
116
|
+
if (schema.format === "uuid" && !UUID.test(str)) return msg("format", "Enter a valid UUID");
|
|
271
117
|
}
|
|
272
118
|
if (schema.kind === "number") {
|
|
273
119
|
const num = Number(value);
|
|
274
|
-
if (Number.isNaN(num)) return msg("Must be a number");
|
|
275
|
-
if (schema.min !== void 0 && num < schema.min)
|
|
276
|
-
|
|
120
|
+
if (Number.isNaN(num)) return msg("type", "Must be a number");
|
|
121
|
+
if (schema.min !== void 0 && num < schema.min)
|
|
122
|
+
return msg("min", `Must be \u2265 ${schema.min}`);
|
|
123
|
+
if (schema.max !== void 0 && num > schema.max)
|
|
124
|
+
return msg("max", `Must be \u2264 ${schema.max}`);
|
|
277
125
|
}
|
|
278
126
|
return null;
|
|
279
127
|
};
|
|
280
128
|
}
|
|
129
|
+
function requiredMessage(schema) {
|
|
130
|
+
return schema?.messages?.required ?? (typeof schema?.message === "string" ? schema.message : "This field is required");
|
|
131
|
+
}
|
|
281
132
|
|
|
282
133
|
// src/providers.ts
|
|
283
134
|
function isProviderRef(value) {
|
|
@@ -317,10 +168,14 @@ function defaultValueFor(type) {
|
|
|
317
168
|
return "";
|
|
318
169
|
}
|
|
319
170
|
}
|
|
171
|
+
var uidSeq = 0;
|
|
320
172
|
var FieldState = class {
|
|
321
173
|
/** Discriminant for the {@link FieldNode} union (leaf vs group/collection). */
|
|
322
174
|
kind = "field";
|
|
323
175
|
id;
|
|
176
|
+
/** A globally-unique DOM id for the control (collection rows reuse `id`, so this must be unique). */
|
|
177
|
+
domId = `fw-${(uidSeq++).toString(36)}`;
|
|
178
|
+
/** The field's schema — mutable at runtime via {@link patchSchema}. */
|
|
324
179
|
schema;
|
|
325
180
|
value;
|
|
326
181
|
error;
|
|
@@ -328,32 +183,55 @@ var FieldState = class {
|
|
|
328
183
|
visible;
|
|
329
184
|
enabled;
|
|
330
185
|
required;
|
|
186
|
+
/** Bumps whenever the schema is patched — renderers re-render the field on change. */
|
|
187
|
+
revision;
|
|
331
188
|
validator;
|
|
332
|
-
|
|
189
|
+
rev = reactive.signal(0);
|
|
190
|
+
stepActive;
|
|
191
|
+
constructor(schema, initial, getValue, stepActive) {
|
|
333
192
|
this.id = schema.id;
|
|
334
193
|
this.schema = schema;
|
|
335
|
-
this.
|
|
336
|
-
this.
|
|
337
|
-
this.
|
|
194
|
+
this.stepActive = stepActive;
|
|
195
|
+
this.value = reactive.signal(initial);
|
|
196
|
+
this.error = reactive.signal(null);
|
|
197
|
+
this.touched = reactive.signal(false);
|
|
338
198
|
this.validator = schema.validation ? compileValidator(schema.validation) : null;
|
|
339
|
-
this.
|
|
340
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
199
|
+
this.revision = this.rev;
|
|
200
|
+
this.visible = reactive.computed(() => {
|
|
201
|
+
this.rev.get();
|
|
202
|
+
return evaluateCondition(this.schema.visibleWhen, getValue, true);
|
|
203
|
+
});
|
|
204
|
+
this.enabled = reactive.computed(() => {
|
|
205
|
+
this.rev.get();
|
|
206
|
+
return evaluateCondition(this.schema.enabledWhen, getValue, true);
|
|
207
|
+
});
|
|
208
|
+
this.required = reactive.computed(() => {
|
|
209
|
+
this.rev.get();
|
|
210
|
+
if (this.schema.requiredWhen !== void 0) {
|
|
211
|
+
return evaluateCondition(this.schema.requiredWhen, getValue, false);
|
|
344
212
|
}
|
|
345
|
-
return schema.validation?.required ?? false;
|
|
213
|
+
return this.schema.validation?.required ?? false;
|
|
346
214
|
});
|
|
347
215
|
}
|
|
348
|
-
/**
|
|
349
|
-
|
|
216
|
+
/** Merge a partial schema in at runtime (change type, label, options, validation, …). */
|
|
217
|
+
patchSchema(partial) {
|
|
218
|
+
this.schema = { ...this.schema, ...partial };
|
|
219
|
+
this.validator = this.schema.validation ? compileValidator(this.schema.validation) : null;
|
|
220
|
+
this.rev.update((n) => n + 1);
|
|
221
|
+
}
|
|
222
|
+
/** Run validation, store and return the error (or null). Hidden / inactive-step fields never error. */
|
|
223
|
+
validate(options) {
|
|
350
224
|
if (!this.visible.peek()) {
|
|
351
225
|
this.error.set(null);
|
|
352
226
|
return null;
|
|
353
227
|
}
|
|
228
|
+
if (!options?.allSteps && this.stepActive && !this.stepActive.peek()) {
|
|
229
|
+
this.error.set(null);
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
354
232
|
let result = null;
|
|
355
233
|
if (this.required.peek() && isEmpty2(this.value.peek())) {
|
|
356
|
-
result =
|
|
234
|
+
result = requiredMessage(this.schema.validation);
|
|
357
235
|
} else if (this.validator) {
|
|
358
236
|
result = this.validator(this.value.peek());
|
|
359
237
|
}
|
|
@@ -371,26 +249,40 @@ function isEmpty2(value) {
|
|
|
371
249
|
}
|
|
372
250
|
|
|
373
251
|
// src/nodes.ts
|
|
252
|
+
var PRESENTATIONAL = /* @__PURE__ */ new Set(["heading", "separator", "paragraph"]);
|
|
253
|
+
function isPresentational(type) {
|
|
254
|
+
return PRESENTATIONAL.has(type);
|
|
255
|
+
}
|
|
374
256
|
function nodeValue(node) {
|
|
375
257
|
return node.value.get();
|
|
376
258
|
}
|
|
377
259
|
function collectValues(nodes) {
|
|
378
260
|
const out = {};
|
|
379
|
-
for (const node of nodes)
|
|
261
|
+
for (const node of nodes) {
|
|
262
|
+
if (node.schema.omit || isPresentational(node.schema.type)) continue;
|
|
263
|
+
if (!node.visible.get()) continue;
|
|
264
|
+
out[node.id] = nodeValue(node);
|
|
265
|
+
}
|
|
380
266
|
return out;
|
|
381
267
|
}
|
|
382
|
-
function buildNodes(schemas, scope, initial) {
|
|
268
|
+
function buildNodes(schemas, scope, initial, stepActive) {
|
|
383
269
|
const nodes = [];
|
|
384
270
|
const byName = /* @__PURE__ */ new Map();
|
|
385
271
|
for (const schema of schemas) {
|
|
386
272
|
let node;
|
|
387
273
|
if (schema.type === "group") {
|
|
388
|
-
node = new GroupNode(schema, scope, asDict(initial[schema.id]));
|
|
274
|
+
node = new GroupNode(schema, scope, asDict(initial[schema.id]), stepActive);
|
|
275
|
+
} else if (schema.type === "step") {
|
|
276
|
+
if (!stepActive)
|
|
277
|
+
throw new Error('Field type "step" must be nested inside a "steps" container');
|
|
278
|
+
node = new StepNode(schema, scope, asDict(initial[schema.id]), stepActive);
|
|
389
279
|
} else if (schema.type === "collection") {
|
|
390
280
|
node = new CollectionNode(schema, scope, asArray(initial[schema.id]));
|
|
281
|
+
} else if (schema.type === "steps") {
|
|
282
|
+
node = new StepsNode(schema, scope, asDict(initial[schema.id]));
|
|
391
283
|
} else {
|
|
392
284
|
const init = initial[schema.id] ?? schema.defaultValue ?? defaultValueFor(schema.type);
|
|
393
|
-
node = new FieldState(schema, init, scope);
|
|
285
|
+
node = new FieldState(schema, init, scope, stepActive);
|
|
394
286
|
}
|
|
395
287
|
nodes.push(node);
|
|
396
288
|
byName.set(schema.id, node);
|
|
@@ -408,7 +300,9 @@ function resetNodes(nodes, initial) {
|
|
|
408
300
|
if (node.kind === "field") {
|
|
409
301
|
const init = initial[node.id] ?? node.schema.defaultValue ?? defaultValueFor(node.schema.type);
|
|
410
302
|
node.reset(init);
|
|
411
|
-
} else if (node.kind === "group") {
|
|
303
|
+
} else if (node.kind === "group" || node.kind === "step") {
|
|
304
|
+
node.reset(asDict(initial[node.id]));
|
|
305
|
+
} else if (node.kind === "steps") {
|
|
412
306
|
node.reset(asDict(initial[node.id]));
|
|
413
307
|
} else {
|
|
414
308
|
node.reset(asArray(initial[node.id]));
|
|
@@ -426,19 +320,19 @@ var GroupNode = class {
|
|
|
426
320
|
enabled;
|
|
427
321
|
/** The scope a child uses: resolve a name among siblings, else delegate upward. */
|
|
428
322
|
scope;
|
|
429
|
-
constructor(schema, parentScope, initial) {
|
|
323
|
+
constructor(schema, parentScope, initial, stepActive) {
|
|
430
324
|
this.id = schema.id;
|
|
431
325
|
this.schema = schema;
|
|
432
326
|
this.scope = (name) => {
|
|
433
327
|
const child = this.byName.get(name);
|
|
434
328
|
return child ? nodeValue(child) : parentScope(name);
|
|
435
329
|
};
|
|
436
|
-
const built = buildNodes(schema.fields ?? [], this.scope, initial);
|
|
330
|
+
const built = buildNodes(schema.fields ?? [], this.scope, initial, stepActive);
|
|
437
331
|
this.children = built.nodes;
|
|
438
332
|
this.byName = built.byName;
|
|
439
|
-
this.value = computed(() => collectValues(this.children));
|
|
440
|
-
this.visible = computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
441
|
-
this.enabled = computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
333
|
+
this.value = reactive.computed(() => collectValues(this.children));
|
|
334
|
+
this.visible = reactive.computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
335
|
+
this.enabled = reactive.computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
442
336
|
}
|
|
443
337
|
reset(initial) {
|
|
444
338
|
resetNodes(this.children, initial);
|
|
@@ -461,10 +355,10 @@ var CollectionNode = class {
|
|
|
461
355
|
this.parentScope = parentScope;
|
|
462
356
|
this.itemSchema = { id: schema.id, type: "group", fields: schema.fields ?? [] };
|
|
463
357
|
const seed = this.seedRows(initial);
|
|
464
|
-
this.rows = signal(seed);
|
|
465
|
-
this.value = computed(() => this.rows.get().map((row) => row.group.value.get()));
|
|
466
|
-
this.visible = computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
467
|
-
this.enabled = computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
358
|
+
this.rows = reactive.signal(seed);
|
|
359
|
+
this.value = reactive.computed(() => this.rows.get().map((row) => row.group.value.get()));
|
|
360
|
+
this.visible = reactive.computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
361
|
+
this.enabled = reactive.computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
468
362
|
}
|
|
469
363
|
/** Reactive list of rows (subscribes the caller to add/remove). */
|
|
470
364
|
get items() {
|
|
@@ -496,6 +390,109 @@ var CollectionNode = class {
|
|
|
496
390
|
this.rows.set(this.seedRows(initial));
|
|
497
391
|
}
|
|
498
392
|
};
|
|
393
|
+
var StepNode = class {
|
|
394
|
+
kind = "step";
|
|
395
|
+
id;
|
|
396
|
+
schema;
|
|
397
|
+
children;
|
|
398
|
+
byName;
|
|
399
|
+
value;
|
|
400
|
+
visible;
|
|
401
|
+
enabled;
|
|
402
|
+
scope;
|
|
403
|
+
/** True when this step is the active step in its parent wizard. */
|
|
404
|
+
active;
|
|
405
|
+
constructor(schema, parentScope, initial, active) {
|
|
406
|
+
this.id = schema.id;
|
|
407
|
+
this.schema = schema;
|
|
408
|
+
this.active = active;
|
|
409
|
+
this.scope = (name) => {
|
|
410
|
+
const child = this.byName.get(name);
|
|
411
|
+
return child ? nodeValue(child) : parentScope(name);
|
|
412
|
+
};
|
|
413
|
+
const built = buildNodes(schema.fields ?? [], this.scope, initial, active);
|
|
414
|
+
this.children = built.nodes;
|
|
415
|
+
this.byName = built.byName;
|
|
416
|
+
this.value = reactive.computed(() => collectValues(this.children));
|
|
417
|
+
this.visible = reactive.computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
418
|
+
this.enabled = reactive.computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
419
|
+
}
|
|
420
|
+
reset(initial) {
|
|
421
|
+
resetNodes(this.children, initial);
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
var StepsNode = class {
|
|
425
|
+
kind = "steps";
|
|
426
|
+
id;
|
|
427
|
+
schema;
|
|
428
|
+
steps;
|
|
429
|
+
byName;
|
|
430
|
+
value;
|
|
431
|
+
visible;
|
|
432
|
+
enabled;
|
|
433
|
+
currentStep;
|
|
434
|
+
scope;
|
|
435
|
+
constructor(schema, parentScope, initial) {
|
|
436
|
+
this.id = schema.id;
|
|
437
|
+
this.schema = schema;
|
|
438
|
+
this.currentStep = reactive.signal(0);
|
|
439
|
+
this.byName = /* @__PURE__ */ new Map();
|
|
440
|
+
this.scope = (name) => {
|
|
441
|
+
const step = this.byName.get(name);
|
|
442
|
+
return step ? step.value.get() : parentScope(name);
|
|
443
|
+
};
|
|
444
|
+
const stepSchemas = schema.fields ?? [];
|
|
445
|
+
const steps = stepSchemas.map((stepSchema, index) => {
|
|
446
|
+
const active = reactive.computed(() => this.currentStep.get() === index);
|
|
447
|
+
const stepInitial = asDict(initial[stepSchema.id]);
|
|
448
|
+
const step = new StepNode(stepSchema, this.scope, stepInitial, active);
|
|
449
|
+
this.byName.set(stepSchema.id, step);
|
|
450
|
+
return step;
|
|
451
|
+
});
|
|
452
|
+
this.steps = steps;
|
|
453
|
+
this.value = reactive.computed(() => {
|
|
454
|
+
const out = {};
|
|
455
|
+
for (const step of this.steps) out[step.id] = step.value.get();
|
|
456
|
+
return out;
|
|
457
|
+
});
|
|
458
|
+
this.visible = reactive.computed(() => evaluateCondition(schema.visibleWhen, parentScope, true));
|
|
459
|
+
this.enabled = reactive.computed(() => evaluateCondition(schema.enabledWhen, parentScope, true));
|
|
460
|
+
}
|
|
461
|
+
/** Validate every leaf in the step at `index` (defaults to the current step). */
|
|
462
|
+
validateStep(index) {
|
|
463
|
+
const i = index ?? this.currentStep.peek();
|
|
464
|
+
const step = this.steps[i];
|
|
465
|
+
if (!step) return true;
|
|
466
|
+
let ok = true;
|
|
467
|
+
eachLeaf([step], (leaf) => {
|
|
468
|
+
if (leaf.validate() !== null) ok = false;
|
|
469
|
+
});
|
|
470
|
+
return ok;
|
|
471
|
+
}
|
|
472
|
+
/** Advance to the next step after optionally validating the current one. Returns false if blocked. */
|
|
473
|
+
next() {
|
|
474
|
+
const validate = this.schema.validateOnNext !== false;
|
|
475
|
+
if (validate && !this.validateStep()) return false;
|
|
476
|
+
const cur = this.currentStep.peek();
|
|
477
|
+
if (cur < this.steps.length - 1) {
|
|
478
|
+
this.currentStep.set(cur + 1);
|
|
479
|
+
return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
}
|
|
483
|
+
/** Go back one step (no validation). */
|
|
484
|
+
prev() {
|
|
485
|
+
this.currentStep.update((i) => Math.max(0, i - 1));
|
|
486
|
+
}
|
|
487
|
+
/** Jump to a step by index (does not validate). */
|
|
488
|
+
goTo(index) {
|
|
489
|
+
if (index >= 0 && index < this.steps.length) this.currentStep.set(index);
|
|
490
|
+
}
|
|
491
|
+
reset(initial) {
|
|
492
|
+
this.currentStep.set(0);
|
|
493
|
+
for (const step of this.steps) step.reset(asDict(initial[step.id]));
|
|
494
|
+
}
|
|
495
|
+
};
|
|
499
496
|
function buildTree(schemas, initial) {
|
|
500
497
|
let byName;
|
|
501
498
|
const scope = (name) => {
|
|
@@ -509,8 +506,10 @@ function buildTree(schemas, initial) {
|
|
|
509
506
|
function eachLeaf(nodes, visit) {
|
|
510
507
|
for (const node of nodes) {
|
|
511
508
|
if (node.kind === "field") visit(node);
|
|
512
|
-
else if (node.kind === "group") eachLeaf(node.children, visit);
|
|
513
|
-
else
|
|
509
|
+
else if (node.kind === "group" || node.kind === "step") eachLeaf(node.children, visit);
|
|
510
|
+
else if (node.kind === "steps") {
|
|
511
|
+
for (const step of node.steps) eachLeaf(step.children, visit);
|
|
512
|
+
} else for (const row of node.items.peek()) eachLeaf(row.group.children, visit);
|
|
514
513
|
}
|
|
515
514
|
}
|
|
516
515
|
var defaultRenderer = null;
|
|
@@ -529,24 +528,37 @@ var Form = class {
|
|
|
529
528
|
isDirty;
|
|
530
529
|
/** True when no visible field currently has an error. */
|
|
531
530
|
isValid;
|
|
532
|
-
submitting = signal(false);
|
|
531
|
+
submitting = reactive.signal(false);
|
|
533
532
|
initialValues;
|
|
534
533
|
initialSnapshot;
|
|
535
534
|
rootByName;
|
|
536
535
|
listeners = /* @__PURE__ */ new Map();
|
|
537
536
|
disposeRenderer = null;
|
|
537
|
+
disposePersist = null;
|
|
538
538
|
constructor(schema$1, initialValues = {}, options = {}) {
|
|
539
539
|
this.schema = schema.parseSchema(schema$1);
|
|
540
540
|
this.options = options;
|
|
541
541
|
this.initialValues = initialValues;
|
|
542
|
-
const
|
|
542
|
+
const fields = this.schema.locales?.length ? expandLocalized(this.schema.fields, this.schema.locales) : this.schema.fields;
|
|
543
|
+
const seed = loadPersisted(options.persistKey, initialValues);
|
|
544
|
+
const tree = buildTree(fields, seed);
|
|
543
545
|
this.tree = tree.nodes;
|
|
544
546
|
this.rootByName = tree.byName;
|
|
545
|
-
this.order =
|
|
546
|
-
this.values = computed(() => collectTree(this.tree));
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
547
|
+
this.order = fields.map((f) => f.id);
|
|
548
|
+
this.values = reactive.computed(() => collectTree(this.tree));
|
|
549
|
+
if (options.persistKey) {
|
|
550
|
+
const key = options.persistKey;
|
|
551
|
+
this.disposePersist = reactive.effect(() => {
|
|
552
|
+
const v = this.values.get();
|
|
553
|
+
try {
|
|
554
|
+
localStorage.setItem(key, JSON.stringify(v));
|
|
555
|
+
} catch {
|
|
556
|
+
}
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
this.initialSnapshot = JSON.stringify(reactive.untrack(() => this.values.peek()));
|
|
560
|
+
this.isDirty = reactive.computed(() => JSON.stringify(this.values.get()) !== this.initialSnapshot);
|
|
561
|
+
this.isValid = reactive.computed(() => {
|
|
550
562
|
let valid = true;
|
|
551
563
|
eachLeaf(this.tree, (leaf) => {
|
|
552
564
|
if (leaf.visible.get() && leaf.error.get() !== null) valid = false;
|
|
@@ -574,14 +586,24 @@ var Form = class {
|
|
|
574
586
|
setFieldValue(field, value) {
|
|
575
587
|
field.value.set(value);
|
|
576
588
|
field.touched.set(true);
|
|
577
|
-
|
|
589
|
+
field.validate();
|
|
578
590
|
this.emit("change", { id: field.id, value });
|
|
579
591
|
}
|
|
580
592
|
setError(id, error) {
|
|
581
593
|
this.field(id)?.error.set(error);
|
|
582
594
|
}
|
|
595
|
+
/** Patch one field's schema at runtime (change type, label, options, validation, …). */
|
|
596
|
+
setFieldSchema(path, partial) {
|
|
597
|
+
this.field(path)?.patchSchema(partial);
|
|
598
|
+
}
|
|
599
|
+
/** Patch many fields' schemas at once: `form.patch({ state: { type: "text" }, … })`. */
|
|
600
|
+
patch(updates) {
|
|
601
|
+
reactive.batch(() => {
|
|
602
|
+
for (const [path, partial] of Object.entries(updates)) this.setFieldSchema(path, partial);
|
|
603
|
+
});
|
|
604
|
+
}
|
|
583
605
|
setErrors(errors) {
|
|
584
|
-
batch(() => {
|
|
606
|
+
reactive.batch(() => {
|
|
585
607
|
for (const [id, error] of Object.entries(errors)) this.setError(id, error);
|
|
586
608
|
});
|
|
587
609
|
}
|
|
@@ -590,47 +612,70 @@ var Form = class {
|
|
|
590
612
|
}
|
|
591
613
|
// ---- lifecycle ----------------------------------------------------------
|
|
592
614
|
/** Validate every (visible) leaf field; returns true when the whole form is valid. */
|
|
593
|
-
validate() {
|
|
594
|
-
|
|
615
|
+
validate(options) {
|
|
616
|
+
const allSteps = options?.allSteps ?? false;
|
|
617
|
+
return reactive.untrack(() => {
|
|
595
618
|
let ok = true;
|
|
596
|
-
batch(() => {
|
|
619
|
+
reactive.batch(() => {
|
|
597
620
|
eachLeaf(this.tree, (leaf) => {
|
|
598
|
-
if (leaf.validate() !== null) ok = false;
|
|
621
|
+
if (leaf.validate({ allSteps }) !== null) ok = false;
|
|
599
622
|
});
|
|
600
623
|
});
|
|
601
624
|
return ok;
|
|
602
625
|
});
|
|
603
626
|
}
|
|
604
|
-
/**
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
627
|
+
/** Find the first `steps` container in the field tree (if any). */
|
|
628
|
+
findSteps() {
|
|
629
|
+
return findSteps(this.tree);
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Run the submission pipeline: validate → transform → send → onSuccess/onError.
|
|
633
|
+
* Pass an inline `transform` to shape the final payload, e.g.
|
|
634
|
+
* `form.submit((values) => ({ ...values, source: "web" }))`.
|
|
635
|
+
*
|
|
636
|
+
* Always **resolves** with a {@link SubmitResult} — never throws — so you can
|
|
637
|
+
* handle both outcomes from the API in one place:
|
|
638
|
+
* `const res = await form.submit(); res.ok ? res.data : res.error`.
|
|
639
|
+
*/
|
|
640
|
+
async submit(transform) {
|
|
641
|
+
if (!this.validate({ allSteps: true })) {
|
|
642
|
+
const errors = this.collectErrors();
|
|
643
|
+
const error = new FormValidationError(errors);
|
|
608
644
|
this.runErrorHandler(error);
|
|
609
645
|
this.emit("error", error);
|
|
610
|
-
|
|
646
|
+
return { ok: false, error, errors };
|
|
611
647
|
}
|
|
612
648
|
this.submitting.set(true);
|
|
613
|
-
const values = untrack(() => this.values.peek());
|
|
614
|
-
const
|
|
649
|
+
const values = reactive.untrack(() => this.values.peek());
|
|
650
|
+
const named = this.applyTransform(values);
|
|
651
|
+
const payload = transform ? transform(named, this) : named;
|
|
615
652
|
this.emit("submit", payload);
|
|
616
653
|
try {
|
|
617
|
-
const
|
|
618
|
-
this.
|
|
619
|
-
this.
|
|
620
|
-
|
|
654
|
+
const data = await this.send(payload);
|
|
655
|
+
this.clearPersisted();
|
|
656
|
+
this.runSuccessHandler(data);
|
|
657
|
+
this.emit("success", data);
|
|
658
|
+
return { ok: true, data };
|
|
621
659
|
} catch (error) {
|
|
622
660
|
this.runErrorHandler(error);
|
|
623
661
|
this.emit("error", error);
|
|
624
|
-
|
|
662
|
+
return { ok: false, error };
|
|
625
663
|
} finally {
|
|
626
664
|
this.submitting.set(false);
|
|
627
665
|
}
|
|
628
666
|
}
|
|
629
667
|
reset(values = this.initialValues) {
|
|
630
|
-
batch(() => {
|
|
668
|
+
reactive.batch(() => {
|
|
631
669
|
resetNodes(this.tree, values);
|
|
632
670
|
});
|
|
633
671
|
}
|
|
672
|
+
/** Trigger a named form action: runs its handler (from options) and emits "action". */
|
|
673
|
+
action(name) {
|
|
674
|
+
const def = this.schema.actions?.find((a) => a.name === name);
|
|
675
|
+
const handler = def?.handler ? this.options.handlers?.[def.handler] : void 0;
|
|
676
|
+
handler?.(this);
|
|
677
|
+
this.emit("action", { name });
|
|
678
|
+
}
|
|
634
679
|
/** Mount into a host element using the given renderer (or the registered default). */
|
|
635
680
|
mount(host, renderer = defaultRenderer) {
|
|
636
681
|
if (!renderer) {
|
|
@@ -645,8 +690,19 @@ var Form = class {
|
|
|
645
690
|
destroy() {
|
|
646
691
|
this.disposeRenderer?.();
|
|
647
692
|
this.disposeRenderer = null;
|
|
693
|
+
this.disposePersist?.();
|
|
694
|
+
this.disposePersist = null;
|
|
648
695
|
this.listeners.clear();
|
|
649
696
|
}
|
|
697
|
+
/** Remove the cached draft from `localStorage` (called on a successful submit). */
|
|
698
|
+
clearPersisted() {
|
|
699
|
+
const key = this.options.persistKey;
|
|
700
|
+
if (!key || typeof localStorage === "undefined") return;
|
|
701
|
+
try {
|
|
702
|
+
localStorage.removeItem(key);
|
|
703
|
+
} catch {
|
|
704
|
+
}
|
|
705
|
+
}
|
|
650
706
|
// ---- events -------------------------------------------------------------
|
|
651
707
|
on(event, listener) {
|
|
652
708
|
let set = this.listeners.get(event);
|
|
@@ -708,14 +764,64 @@ var FormValidationError = class extends Error {
|
|
|
708
764
|
function collectTree(tree) {
|
|
709
765
|
return collectValues(tree);
|
|
710
766
|
}
|
|
767
|
+
function loadPersisted(key, initial) {
|
|
768
|
+
if (!key || typeof localStorage === "undefined") return initial;
|
|
769
|
+
try {
|
|
770
|
+
const saved = localStorage.getItem(key);
|
|
771
|
+
if (saved) return { ...initial, ...JSON.parse(saved) };
|
|
772
|
+
} catch {
|
|
773
|
+
}
|
|
774
|
+
return initial;
|
|
775
|
+
}
|
|
776
|
+
function expandLocalized(fields, locales) {
|
|
777
|
+
return fields.map((f) => {
|
|
778
|
+
if (f.localized) {
|
|
779
|
+
const leafType = f.type === "group" || f.type === "collection" ? "text" : f.type;
|
|
780
|
+
const child = (loc) => {
|
|
781
|
+
const c = { id: loc, type: leafType, label: loc };
|
|
782
|
+
if (f.placeholder !== void 0) c["placeholder"] = f.placeholder;
|
|
783
|
+
if (f.validation !== void 0) c["validation"] = f.validation;
|
|
784
|
+
if (f.options !== void 0) c["options"] = f.options;
|
|
785
|
+
if (f.widget !== void 0) c["widget"] = f.widget;
|
|
786
|
+
if (f.tooltip !== void 0) c["tooltip"] = f.tooltip;
|
|
787
|
+
return c;
|
|
788
|
+
};
|
|
789
|
+
const group = {
|
|
790
|
+
id: f.id,
|
|
791
|
+
type: "group",
|
|
792
|
+
// Keep the `localized` flag so the renderer shows ONE input + a language
|
|
793
|
+
// switcher (instead of one input per locale). Value stays `{ en, ar }`.
|
|
794
|
+
localized: true,
|
|
795
|
+
fields: locales.map(child)
|
|
796
|
+
};
|
|
797
|
+
if (f.defaultLocale !== void 0) group["defaultLocale"] = f.defaultLocale;
|
|
798
|
+
for (const key of [
|
|
799
|
+
"label",
|
|
800
|
+
"visibleWhen",
|
|
801
|
+
"enabledWhen",
|
|
802
|
+
"class",
|
|
803
|
+
"classes",
|
|
804
|
+
"help",
|
|
805
|
+
"tooltip"
|
|
806
|
+
]) {
|
|
807
|
+
if (f[key] !== void 0) group[key] = f[key];
|
|
808
|
+
}
|
|
809
|
+
return group;
|
|
810
|
+
}
|
|
811
|
+
if (f.fields) return { ...f, fields: expandLocalized(f.fields, locales) };
|
|
812
|
+
return f;
|
|
813
|
+
});
|
|
814
|
+
}
|
|
711
815
|
function collectLeaves(tree) {
|
|
712
816
|
const out = /* @__PURE__ */ new Map();
|
|
713
817
|
const walk = (nodes, prefix) => {
|
|
714
818
|
for (const node of nodes) {
|
|
715
819
|
const path = prefix ? `${prefix}.${node.id}` : node.id;
|
|
716
820
|
if (node.kind === "field") out.set(path, node);
|
|
717
|
-
else if (node.kind === "group") walk(node.children, path);
|
|
718
|
-
else {
|
|
821
|
+
else if (node.kind === "group" || node.kind === "step") walk(node.children, path);
|
|
822
|
+
else if (node.kind === "steps") {
|
|
823
|
+
for (const step of node.steps) walk(step.children, `${path}.${step.id}`);
|
|
824
|
+
} else {
|
|
719
825
|
node.items.peek().forEach((row, i) => walk(row.group.children, `${path}.${i}`));
|
|
720
826
|
}
|
|
721
827
|
}
|
|
@@ -728,7 +834,9 @@ function resolveLeaf(tree, rootByName, path) {
|
|
|
728
834
|
let node = rootByName.get(parts[0]);
|
|
729
835
|
for (let i = 1; i < parts.length && node; i++) {
|
|
730
836
|
const part = parts[i];
|
|
731
|
-
if (node instanceof GroupNode) {
|
|
837
|
+
if (node instanceof GroupNode || node instanceof StepNode) {
|
|
838
|
+
node = node.byName.get(part);
|
|
839
|
+
} else if (node instanceof StepsNode) {
|
|
732
840
|
node = node.byName.get(part);
|
|
733
841
|
} else if (node instanceof CollectionNode) {
|
|
734
842
|
node = node.items.peek()[Number(part)]?.group;
|
|
@@ -738,27 +846,63 @@ function resolveLeaf(tree, rootByName, path) {
|
|
|
738
846
|
}
|
|
739
847
|
return node && node.kind === "field" ? node : void 0;
|
|
740
848
|
}
|
|
849
|
+
function findSteps(tree) {
|
|
850
|
+
for (const node of tree) {
|
|
851
|
+
if (node.kind === "steps") return node;
|
|
852
|
+
if (node.kind === "group" || node.kind === "step") {
|
|
853
|
+
const found = findSteps(node.children);
|
|
854
|
+
if (found) return found;
|
|
855
|
+
} else if (node.kind === "collection") {
|
|
856
|
+
for (const row of node.items.peek()) {
|
|
857
|
+
const found = findSteps(row.group.children);
|
|
858
|
+
if (found) return found;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
return void 0;
|
|
863
|
+
}
|
|
741
864
|
|
|
865
|
+
Object.defineProperty(exports, "batch", {
|
|
866
|
+
enumerable: true,
|
|
867
|
+
get: function () { return reactive.batch; }
|
|
868
|
+
});
|
|
869
|
+
Object.defineProperty(exports, "computed", {
|
|
870
|
+
enumerable: true,
|
|
871
|
+
get: function () { return reactive.computed; }
|
|
872
|
+
});
|
|
873
|
+
Object.defineProperty(exports, "effect", {
|
|
874
|
+
enumerable: true,
|
|
875
|
+
get: function () { return reactive.effect; }
|
|
876
|
+
});
|
|
877
|
+
Object.defineProperty(exports, "isTracking", {
|
|
878
|
+
enumerable: true,
|
|
879
|
+
get: function () { return reactive.isTracking; }
|
|
880
|
+
});
|
|
881
|
+
Object.defineProperty(exports, "signal", {
|
|
882
|
+
enumerable: true,
|
|
883
|
+
get: function () { return reactive.signal; }
|
|
884
|
+
});
|
|
885
|
+
Object.defineProperty(exports, "untrack", {
|
|
886
|
+
enumerable: true,
|
|
887
|
+
get: function () { return reactive.untrack; }
|
|
888
|
+
});
|
|
742
889
|
exports.CollectionNode = CollectionNode;
|
|
743
890
|
exports.FieldState = FieldState;
|
|
744
891
|
exports.Form = Form;
|
|
745
892
|
exports.FormValidationError = FormValidationError;
|
|
746
893
|
exports.GroupNode = GroupNode;
|
|
747
|
-
exports.
|
|
894
|
+
exports.StepNode = StepNode;
|
|
895
|
+
exports.StepsNode = StepsNode;
|
|
748
896
|
exports.buildTree = buildTree;
|
|
749
897
|
exports.compileValidator = compileValidator;
|
|
750
|
-
exports.computed = computed;
|
|
751
898
|
exports.defaultValueFor = defaultValueFor;
|
|
752
899
|
exports.eachLeaf = eachLeaf;
|
|
753
|
-
exports.effect = effect;
|
|
754
900
|
exports.evaluateCondition = evaluateCondition;
|
|
901
|
+
exports.isPresentational = isPresentational;
|
|
755
902
|
exports.isProviderRef = isProviderRef;
|
|
756
|
-
exports.isTracking = isTracking;
|
|
757
903
|
exports.referencedFields = referencedFields;
|
|
758
904
|
exports.resolve = resolve;
|
|
759
905
|
exports.resolveQuery = resolveQuery;
|
|
760
906
|
exports.setDefaultRenderer = setDefaultRenderer;
|
|
761
|
-
exports.signal = signal;
|
|
762
|
-
exports.untrack = untrack;
|
|
763
907
|
//# sourceMappingURL=index.cjs.map
|
|
764
908
|
//# sourceMappingURL=index.cjs.map
|