@formwright/core 0.1.0 → 0.2.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/README.md +96 -0
- package/dist/chunk-O4DUMDBU.js +3 -0
- package/dist/chunk-O4DUMDBU.js.map +1 -0
- package/dist/index.cjs +214 -215
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +51 -9
- package/dist/index.d.ts +51 -9
- package/dist/index.js +170 -34
- 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.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { signal, computed, untrack, batch } from './chunk-
|
|
2
|
-
export { batch, computed, effect, isTracking, signal, untrack } from './chunk-
|
|
1
|
+
import { signal, computed, effect, untrack, batch } from './chunk-O4DUMDBU.js';
|
|
2
|
+
export { batch, computed, effect, isTracking, signal, untrack } from './chunk-O4DUMDBU.js';
|
|
3
3
|
import { parseSchema } from '@formwright/schema';
|
|
4
4
|
|
|
5
5
|
// src/conditions.ts
|
|
@@ -90,35 +90,42 @@ function isEmpty(value) {
|
|
|
90
90
|
return value === void 0 || value === null || value === "";
|
|
91
91
|
}
|
|
92
92
|
function compileValidator(schema) {
|
|
93
|
+
const overrides = schema.messages ?? {};
|
|
94
|
+
const msg = (rule, fallback) => overrides[rule] ?? (typeof schema.message === "string" ? schema.message : fallback);
|
|
93
95
|
return (value) => {
|
|
94
|
-
const msg = (fallback) => typeof schema.message === "string" ? schema.message : fallback;
|
|
95
96
|
if (isEmpty(value)) {
|
|
96
|
-
return schema.required ? msg("This field is required") : null;
|
|
97
|
+
return schema.required ? msg("required", "This field is required") : null;
|
|
97
98
|
}
|
|
98
99
|
if (schema.kind === "string" || schema.format) {
|
|
99
100
|
const str = String(value);
|
|
100
101
|
if (schema.minLength !== void 0 && str.length < schema.minLength) {
|
|
101
|
-
return msg(`Must be at least ${schema.minLength} characters`);
|
|
102
|
+
return msg("minLength", `Must be at least ${schema.minLength} characters`);
|
|
102
103
|
}
|
|
103
104
|
if (schema.maxLength !== void 0 && str.length > schema.maxLength) {
|
|
104
|
-
return msg(`Must be at most ${schema.maxLength} characters`);
|
|
105
|
+
return msg("maxLength", `Must be at most ${schema.maxLength} characters`);
|
|
105
106
|
}
|
|
106
107
|
if (schema.pattern !== void 0 && !new RegExp(schema.pattern).test(str)) {
|
|
107
|
-
return msg("Invalid format");
|
|
108
|
+
return msg("pattern", "Invalid format");
|
|
108
109
|
}
|
|
109
|
-
if (schema.format === "email" && !EMAIL.test(str))
|
|
110
|
-
|
|
111
|
-
if (schema.format === "
|
|
110
|
+
if (schema.format === "email" && !EMAIL.test(str))
|
|
111
|
+
return msg("format", "Enter a valid email");
|
|
112
|
+
if (schema.format === "url" && !URL.test(str)) return msg("format", "Enter a valid URL");
|
|
113
|
+
if (schema.format === "uuid" && !UUID.test(str)) return msg("format", "Enter a valid UUID");
|
|
112
114
|
}
|
|
113
115
|
if (schema.kind === "number") {
|
|
114
116
|
const num = Number(value);
|
|
115
|
-
if (Number.isNaN(num)) return msg("Must be a number");
|
|
116
|
-
if (schema.min !== void 0 && num < schema.min)
|
|
117
|
-
|
|
117
|
+
if (Number.isNaN(num)) return msg("type", "Must be a number");
|
|
118
|
+
if (schema.min !== void 0 && num < schema.min)
|
|
119
|
+
return msg("min", `Must be \u2265 ${schema.min}`);
|
|
120
|
+
if (schema.max !== void 0 && num > schema.max)
|
|
121
|
+
return msg("max", `Must be \u2264 ${schema.max}`);
|
|
118
122
|
}
|
|
119
123
|
return null;
|
|
120
124
|
};
|
|
121
125
|
}
|
|
126
|
+
function requiredMessage(schema) {
|
|
127
|
+
return schema?.messages?.required ?? (typeof schema?.message === "string" ? schema.message : "This field is required");
|
|
128
|
+
}
|
|
122
129
|
|
|
123
130
|
// src/providers.ts
|
|
124
131
|
function isProviderRef(value) {
|
|
@@ -158,10 +165,14 @@ function defaultValueFor(type) {
|
|
|
158
165
|
return "";
|
|
159
166
|
}
|
|
160
167
|
}
|
|
168
|
+
var uidSeq = 0;
|
|
161
169
|
var FieldState = class {
|
|
162
170
|
/** Discriminant for the {@link FieldNode} union (leaf vs group/collection). */
|
|
163
171
|
kind = "field";
|
|
164
172
|
id;
|
|
173
|
+
/** A globally-unique DOM id for the control (collection rows reuse `id`, so this must be unique). */
|
|
174
|
+
domId = `fw-${(uidSeq++).toString(36)}`;
|
|
175
|
+
/** The field's schema — mutable at runtime via {@link patchSchema}. */
|
|
165
176
|
schema;
|
|
166
177
|
value;
|
|
167
178
|
error;
|
|
@@ -169,7 +180,10 @@ var FieldState = class {
|
|
|
169
180
|
visible;
|
|
170
181
|
enabled;
|
|
171
182
|
required;
|
|
183
|
+
/** Bumps whenever the schema is patched — renderers re-render the field on change. */
|
|
184
|
+
revision;
|
|
172
185
|
validator;
|
|
186
|
+
rev = signal(0);
|
|
173
187
|
constructor(schema, initial, getValue) {
|
|
174
188
|
this.id = schema.id;
|
|
175
189
|
this.schema = schema;
|
|
@@ -177,15 +191,29 @@ var FieldState = class {
|
|
|
177
191
|
this.error = signal(null);
|
|
178
192
|
this.touched = signal(false);
|
|
179
193
|
this.validator = schema.validation ? compileValidator(schema.validation) : null;
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
194
|
+
this.revision = this.rev;
|
|
195
|
+
this.visible = computed(() => {
|
|
196
|
+
this.rev.get();
|
|
197
|
+
return evaluateCondition(this.schema.visibleWhen, getValue, true);
|
|
198
|
+
});
|
|
199
|
+
this.enabled = computed(() => {
|
|
200
|
+
this.rev.get();
|
|
201
|
+
return evaluateCondition(this.schema.enabledWhen, getValue, true);
|
|
202
|
+
});
|
|
182
203
|
this.required = computed(() => {
|
|
183
|
-
|
|
184
|
-
|
|
204
|
+
this.rev.get();
|
|
205
|
+
if (this.schema.requiredWhen !== void 0) {
|
|
206
|
+
return evaluateCondition(this.schema.requiredWhen, getValue, false);
|
|
185
207
|
}
|
|
186
|
-
return schema.validation?.required ?? false;
|
|
208
|
+
return this.schema.validation?.required ?? false;
|
|
187
209
|
});
|
|
188
210
|
}
|
|
211
|
+
/** Merge a partial schema in at runtime (change type, label, options, validation, …). */
|
|
212
|
+
patchSchema(partial) {
|
|
213
|
+
this.schema = { ...this.schema, ...partial };
|
|
214
|
+
this.validator = this.schema.validation ? compileValidator(this.schema.validation) : null;
|
|
215
|
+
this.rev.update((n) => n + 1);
|
|
216
|
+
}
|
|
189
217
|
/** Run validation, store and return the error (or null). Hidden fields never error. */
|
|
190
218
|
validate() {
|
|
191
219
|
if (!this.visible.peek()) {
|
|
@@ -194,7 +222,7 @@ var FieldState = class {
|
|
|
194
222
|
}
|
|
195
223
|
let result = null;
|
|
196
224
|
if (this.required.peek() && isEmpty2(this.value.peek())) {
|
|
197
|
-
result =
|
|
225
|
+
result = requiredMessage(this.schema.validation);
|
|
198
226
|
} else if (this.validator) {
|
|
199
227
|
result = this.validator(this.value.peek());
|
|
200
228
|
}
|
|
@@ -212,12 +240,20 @@ function isEmpty2(value) {
|
|
|
212
240
|
}
|
|
213
241
|
|
|
214
242
|
// src/nodes.ts
|
|
243
|
+
var PRESENTATIONAL = /* @__PURE__ */ new Set(["heading", "separator", "paragraph"]);
|
|
244
|
+
function isPresentational(type) {
|
|
245
|
+
return PRESENTATIONAL.has(type);
|
|
246
|
+
}
|
|
215
247
|
function nodeValue(node) {
|
|
216
248
|
return node.value.get();
|
|
217
249
|
}
|
|
218
250
|
function collectValues(nodes) {
|
|
219
251
|
const out = {};
|
|
220
|
-
for (const node of nodes)
|
|
252
|
+
for (const node of nodes) {
|
|
253
|
+
if (node.schema.omit || isPresentational(node.schema.type)) continue;
|
|
254
|
+
if (!node.visible.get()) continue;
|
|
255
|
+
out[node.id] = nodeValue(node);
|
|
256
|
+
}
|
|
221
257
|
return out;
|
|
222
258
|
}
|
|
223
259
|
function buildNodes(schemas, scope, initial) {
|
|
@@ -376,15 +412,28 @@ var Form = class {
|
|
|
376
412
|
rootByName;
|
|
377
413
|
listeners = /* @__PURE__ */ new Map();
|
|
378
414
|
disposeRenderer = null;
|
|
415
|
+
disposePersist = null;
|
|
379
416
|
constructor(schema, initialValues = {}, options = {}) {
|
|
380
417
|
this.schema = parseSchema(schema);
|
|
381
418
|
this.options = options;
|
|
382
419
|
this.initialValues = initialValues;
|
|
383
|
-
const
|
|
420
|
+
const fields = this.schema.locales?.length ? expandLocalized(this.schema.fields, this.schema.locales) : this.schema.fields;
|
|
421
|
+
const seed = loadPersisted(options.persistKey, initialValues);
|
|
422
|
+
const tree = buildTree(fields, seed);
|
|
384
423
|
this.tree = tree.nodes;
|
|
385
424
|
this.rootByName = tree.byName;
|
|
386
|
-
this.order =
|
|
425
|
+
this.order = fields.map((f) => f.id);
|
|
387
426
|
this.values = computed(() => collectTree(this.tree));
|
|
427
|
+
if (options.persistKey) {
|
|
428
|
+
const key = options.persistKey;
|
|
429
|
+
this.disposePersist = effect(() => {
|
|
430
|
+
const v = this.values.get();
|
|
431
|
+
try {
|
|
432
|
+
localStorage.setItem(key, JSON.stringify(v));
|
|
433
|
+
} catch {
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
388
437
|
this.initialSnapshot = JSON.stringify(untrack(() => this.values.peek()));
|
|
389
438
|
this.isDirty = computed(() => JSON.stringify(this.values.get()) !== this.initialSnapshot);
|
|
390
439
|
this.isValid = computed(() => {
|
|
@@ -415,12 +464,22 @@ var Form = class {
|
|
|
415
464
|
setFieldValue(field, value) {
|
|
416
465
|
field.value.set(value);
|
|
417
466
|
field.touched.set(true);
|
|
418
|
-
|
|
467
|
+
field.validate();
|
|
419
468
|
this.emit("change", { id: field.id, value });
|
|
420
469
|
}
|
|
421
470
|
setError(id, error) {
|
|
422
471
|
this.field(id)?.error.set(error);
|
|
423
472
|
}
|
|
473
|
+
/** Patch one field's schema at runtime (change type, label, options, validation, …). */
|
|
474
|
+
setFieldSchema(path, partial) {
|
|
475
|
+
this.field(path)?.patchSchema(partial);
|
|
476
|
+
}
|
|
477
|
+
/** Patch many fields' schemas at once: `form.patch({ state: { type: "text" }, … })`. */
|
|
478
|
+
patch(updates) {
|
|
479
|
+
batch(() => {
|
|
480
|
+
for (const [path, partial] of Object.entries(updates)) this.setFieldSchema(path, partial);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
424
483
|
setErrors(errors) {
|
|
425
484
|
batch(() => {
|
|
426
485
|
for (const [id, error] of Object.entries(errors)) this.setError(id, error);
|
|
@@ -442,27 +501,38 @@ var Form = class {
|
|
|
442
501
|
return ok;
|
|
443
502
|
});
|
|
444
503
|
}
|
|
445
|
-
/**
|
|
446
|
-
|
|
504
|
+
/**
|
|
505
|
+
* Run the submission pipeline: validate → transform → send → onSuccess/onError.
|
|
506
|
+
* Pass an inline `transform` to shape the final payload, e.g.
|
|
507
|
+
* `form.submit((values) => ({ ...values, source: "web" }))`.
|
|
508
|
+
*
|
|
509
|
+
* Always **resolves** with a {@link SubmitResult} — never throws — so you can
|
|
510
|
+
* handle both outcomes from the API in one place:
|
|
511
|
+
* `const res = await form.submit(); res.ok ? res.data : res.error`.
|
|
512
|
+
*/
|
|
513
|
+
async submit(transform) {
|
|
447
514
|
if (!this.validate()) {
|
|
448
|
-
const
|
|
515
|
+
const errors = this.collectErrors();
|
|
516
|
+
const error = new FormValidationError(errors);
|
|
449
517
|
this.runErrorHandler(error);
|
|
450
518
|
this.emit("error", error);
|
|
451
|
-
|
|
519
|
+
return { ok: false, error, errors };
|
|
452
520
|
}
|
|
453
521
|
this.submitting.set(true);
|
|
454
522
|
const values = untrack(() => this.values.peek());
|
|
455
|
-
const
|
|
523
|
+
const named = this.applyTransform(values);
|
|
524
|
+
const payload = transform ? transform(named, this) : named;
|
|
456
525
|
this.emit("submit", payload);
|
|
457
526
|
try {
|
|
458
|
-
const
|
|
459
|
-
this.
|
|
460
|
-
this.
|
|
461
|
-
|
|
527
|
+
const data = await this.send(payload);
|
|
528
|
+
this.clearPersisted();
|
|
529
|
+
this.runSuccessHandler(data);
|
|
530
|
+
this.emit("success", data);
|
|
531
|
+
return { ok: true, data };
|
|
462
532
|
} catch (error) {
|
|
463
533
|
this.runErrorHandler(error);
|
|
464
534
|
this.emit("error", error);
|
|
465
|
-
|
|
535
|
+
return { ok: false, error };
|
|
466
536
|
} finally {
|
|
467
537
|
this.submitting.set(false);
|
|
468
538
|
}
|
|
@@ -472,6 +542,13 @@ var Form = class {
|
|
|
472
542
|
resetNodes(this.tree, values);
|
|
473
543
|
});
|
|
474
544
|
}
|
|
545
|
+
/** Trigger a named form action: runs its handler (from options) and emits "action". */
|
|
546
|
+
action(name) {
|
|
547
|
+
const def = this.schema.actions?.find((a) => a.name === name);
|
|
548
|
+
const handler = def?.handler ? this.options.handlers?.[def.handler] : void 0;
|
|
549
|
+
handler?.(this);
|
|
550
|
+
this.emit("action", { name });
|
|
551
|
+
}
|
|
475
552
|
/** Mount into a host element using the given renderer (or the registered default). */
|
|
476
553
|
mount(host, renderer = defaultRenderer) {
|
|
477
554
|
if (!renderer) {
|
|
@@ -486,8 +563,19 @@ var Form = class {
|
|
|
486
563
|
destroy() {
|
|
487
564
|
this.disposeRenderer?.();
|
|
488
565
|
this.disposeRenderer = null;
|
|
566
|
+
this.disposePersist?.();
|
|
567
|
+
this.disposePersist = null;
|
|
489
568
|
this.listeners.clear();
|
|
490
569
|
}
|
|
570
|
+
/** Remove the cached draft from `localStorage` (called on a successful submit). */
|
|
571
|
+
clearPersisted() {
|
|
572
|
+
const key = this.options.persistKey;
|
|
573
|
+
if (!key || typeof localStorage === "undefined") return;
|
|
574
|
+
try {
|
|
575
|
+
localStorage.removeItem(key);
|
|
576
|
+
} catch {
|
|
577
|
+
}
|
|
578
|
+
}
|
|
491
579
|
// ---- events -------------------------------------------------------------
|
|
492
580
|
on(event, listener) {
|
|
493
581
|
let set = this.listeners.get(event);
|
|
@@ -549,6 +637,54 @@ var FormValidationError = class extends Error {
|
|
|
549
637
|
function collectTree(tree) {
|
|
550
638
|
return collectValues(tree);
|
|
551
639
|
}
|
|
640
|
+
function loadPersisted(key, initial) {
|
|
641
|
+
if (!key || typeof localStorage === "undefined") return initial;
|
|
642
|
+
try {
|
|
643
|
+
const saved = localStorage.getItem(key);
|
|
644
|
+
if (saved) return { ...initial, ...JSON.parse(saved) };
|
|
645
|
+
} catch {
|
|
646
|
+
}
|
|
647
|
+
return initial;
|
|
648
|
+
}
|
|
649
|
+
function expandLocalized(fields, locales) {
|
|
650
|
+
return fields.map((f) => {
|
|
651
|
+
if (f.localized) {
|
|
652
|
+
const leafType = f.type === "group" || f.type === "collection" ? "text" : f.type;
|
|
653
|
+
const child = (loc) => {
|
|
654
|
+
const c = { id: loc, type: leafType, label: loc };
|
|
655
|
+
if (f.placeholder !== void 0) c["placeholder"] = f.placeholder;
|
|
656
|
+
if (f.validation !== void 0) c["validation"] = f.validation;
|
|
657
|
+
if (f.options !== void 0) c["options"] = f.options;
|
|
658
|
+
if (f.widget !== void 0) c["widget"] = f.widget;
|
|
659
|
+
if (f.tooltip !== void 0) c["tooltip"] = f.tooltip;
|
|
660
|
+
return c;
|
|
661
|
+
};
|
|
662
|
+
const group = {
|
|
663
|
+
id: f.id,
|
|
664
|
+
type: "group",
|
|
665
|
+
// Keep the `localized` flag so the renderer shows ONE input + a language
|
|
666
|
+
// switcher (instead of one input per locale). Value stays `{ en, ar }`.
|
|
667
|
+
localized: true,
|
|
668
|
+
fields: locales.map(child)
|
|
669
|
+
};
|
|
670
|
+
if (f.defaultLocale !== void 0) group["defaultLocale"] = f.defaultLocale;
|
|
671
|
+
for (const key of [
|
|
672
|
+
"label",
|
|
673
|
+
"visibleWhen",
|
|
674
|
+
"enabledWhen",
|
|
675
|
+
"class",
|
|
676
|
+
"classes",
|
|
677
|
+
"help",
|
|
678
|
+
"tooltip"
|
|
679
|
+
]) {
|
|
680
|
+
if (f[key] !== void 0) group[key] = f[key];
|
|
681
|
+
}
|
|
682
|
+
return group;
|
|
683
|
+
}
|
|
684
|
+
if (f.fields) return { ...f, fields: expandLocalized(f.fields, locales) };
|
|
685
|
+
return f;
|
|
686
|
+
});
|
|
687
|
+
}
|
|
552
688
|
function collectLeaves(tree) {
|
|
553
689
|
const out = /* @__PURE__ */ new Map();
|
|
554
690
|
const walk = (nodes, prefix) => {
|
|
@@ -580,6 +716,6 @@ function resolveLeaf(tree, rootByName, path) {
|
|
|
580
716
|
return node && node.kind === "field" ? node : void 0;
|
|
581
717
|
}
|
|
582
718
|
|
|
583
|
-
export { CollectionNode, FieldState, Form, FormValidationError, GroupNode, buildTree, compileValidator, defaultValueFor, eachLeaf, evaluateCondition, isProviderRef, referencedFields, resolve, resolveQuery, setDefaultRenderer };
|
|
719
|
+
export { CollectionNode, FieldState, Form, FormValidationError, GroupNode, buildTree, compileValidator, defaultValueFor, eachLeaf, evaluateCondition, isPresentational, isProviderRef, referencedFields, resolve, resolveQuery, setDefaultRenderer };
|
|
584
720
|
//# sourceMappingURL=index.js.map
|
|
585
721
|
//# sourceMappingURL=index.js.map
|