@menapo/forms 0.0.0-stage → 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/README.md +45 -2
- package/dist/array.d.ts +5 -0
- package/dist/array.js +101 -0
- package/dist/checks.d.ts +4 -0
- package/dist/checks.js +53 -0
- package/dist/factories/fn.d.ts +8 -0
- package/dist/factories/fn.js +5 -0
- package/dist/factories/route.d.ts +5 -0
- package/dist/factories/route.js +31 -0
- package/dist/form-BZ2OH06z.js +48 -0
- package/dist/glue/preact.d.ts +4 -0
- package/dist/glue/preact.js +59 -0
- package/dist/glue/react.d.ts +4 -0
- package/dist/glue/react.js +55 -0
- package/dist/glue/solid.d.ts +4 -0
- package/dist/glue/solid.js +48 -0
- package/dist/glue/svelte.svelte.d.ts +4 -0
- package/dist/glue/svelte.svelte.js +62 -0
- package/dist/glue/vanilla.d.ts +4 -0
- package/dist/glue/vanilla.js +88 -0
- package/dist/glue/vue.d.ts +4 -0
- package/dist/glue/vue.js +47 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +30 -0
- package/dist/instance-C1aijv06.js +683 -0
- package/dist/names-D_TE3BpM.js +62 -0
- package/dist/paths-DqNafpnl.js +73 -0
- package/dist/route/files.d.ts +5 -0
- package/dist/route/files.js +34 -0
- package/dist/server.d.ts +25 -0
- package/dist/server.js +79 -0
- package/dist/source-DdFTPjID.js +15 -0
- package/dist/types-CdCFdlFp.d.ts +200 -0
- package/dist/view--JGPLHEo.js +308 -0
- package/dist/view-5_kyMWP3.d.ts +331 -0
- package/dist/vite.d.ts +20 -0
- package/dist/vite.js +1154 -0
- package/package.json +98 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 menapo
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,46 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @menapo/forms
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Form state for kit apps: `form("PATCH /projects/:id")` is a form typed and
|
|
4
|
+
validated by the app's own `@menapo/api` operation, submitted through
|
|
5
|
+
`@menapo/query`'s mutation, with the server's field issues on their fields;
|
|
6
|
+
`form(schema, submit)` is one for anything else. Controls are uncontrolled
|
|
7
|
+
and the `<form>` listens once, so typing re-renders nothing that doesn't
|
|
8
|
+
show the change; every control gets its ids and `aria-*` from its parts,
|
|
9
|
+
and a failed submit moves focus. Read with `useForm` in React, Preact, Vue,
|
|
10
|
+
Svelte, Solid and plain code.
|
|
11
|
+
|
|
12
|
+
**Status: built, private until 1.0.** Definitions, validation timing,
|
|
13
|
+
server issues, defaults that follow a query, rows, uploads, checks, the
|
|
14
|
+
plugin (the browser's copy of each operation's input schema, cut from its
|
|
15
|
+
module; constraint attributes from its JSON Schema; glue for six
|
|
16
|
+
frameworks), `/server`'s `action()` and the DevTools panel are implemented
|
|
17
|
+
and tested; `examples/forms-react` and `examples/forms-svelte` run in
|
|
18
|
+
Chromium with axe. The release waits on the owner's review.
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// forms.ts: no framework, no second schema
|
|
22
|
+
export const newOrg = form("POST /orgs");
|
|
23
|
+
export const contact = form(Contact, (values, { signal }) => send(values, signal));
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
const f = useForm(newOrg, { onSuccess: (org) => navigate(`/${org.slug}`) });
|
|
28
|
+
const { name, slug } = f.fields;
|
|
29
|
+
<form {...f.form}>
|
|
30
|
+
<label {...name.label}>Name</label>
|
|
31
|
+
<input {...name.control} />
|
|
32
|
+
<p {...name.error} />
|
|
33
|
+
<label {...slug.label}>Address</label>
|
|
34
|
+
<input {...slug.control} />
|
|
35
|
+
<p {...slug.hint}>Lowercase letters and dashes</p>
|
|
36
|
+
<p {...slug.error} />
|
|
37
|
+
<button {...f.submit}>Create</button>
|
|
38
|
+
</form>;
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- [Guide](docs/guide/README.md): getting started and the reference.
|
|
42
|
+
- [Plan](docs/PLAN.md): the design, the decisions and the measurements.
|
|
43
|
+
- Entries: `@menapo/forms` (`form`, `useForm`, `inspect`, types),
|
|
44
|
+
`@menapo/forms/server` (`action`, `decode`), `@menapo/forms/vite`.
|
|
45
|
+
|
|
46
|
+
MIT licensed.
|
package/dist/array.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { n as Shape, t as FormViewImpl } from "./view-5_kyMWP3.js";
|
|
2
|
+
//#region src/array/index.d.ts
|
|
3
|
+
/** The field of the array at `path` (`Meta.a`): a field, and its items. */
|
|
4
|
+
export declare function arrayField(form: FormViewImpl, path: string, item: Shape): object;
|
|
5
|
+
//#endregion
|
package/dist/array.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { n as get, s as set } from "./paths-DqNafpnl.js";
|
|
2
|
+
import { i as fieldAt, n as at, t as FieldViewImpl } from "./view--JGPLHEo.js";
|
|
3
|
+
//#region src/array/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Field arrays (D14): `f.fields.invites` is a field with stable `keys` to
|
|
6
|
+
* render rows by, its `items`' fields, and `append`, `insert`, `remove`
|
|
7
|
+
* and `move`. A field's state (error, touched, dirty) follows its item,
|
|
8
|
+
* not its index: the records under a moved item are renamed with it, and
|
|
9
|
+
* its row keeps its key, so the uncontrolled controls keep their text. A
|
|
10
|
+
* module of its own, which the plugin hands only a form whose input has an
|
|
11
|
+
* array, so a form without one ships none of it (AGENTS.md: features are
|
|
12
|
+
* modules).
|
|
13
|
+
*/
|
|
14
|
+
let next = 0;
|
|
15
|
+
/** The keys of the array at `path`, as many as it has items. */
|
|
16
|
+
function keysOf(i, path) {
|
|
17
|
+
const all = i.ak ??= /* @__PURE__ */ new Map();
|
|
18
|
+
let keys = all.get(path);
|
|
19
|
+
if (!keys) all.set(path, keys = []);
|
|
20
|
+
const length = get(i.v, path)?.length ?? 0;
|
|
21
|
+
while (keys.length < length) keys.push(`k${++next}`);
|
|
22
|
+
keys.length = length;
|
|
23
|
+
return keys;
|
|
24
|
+
}
|
|
25
|
+
/** Renames what lives under `path.<index>` to where `order` now has that index; gone, it drops. */
|
|
26
|
+
function remap(i, path, order) {
|
|
27
|
+
const prefix = `${path}.`;
|
|
28
|
+
const rename = (p) => {
|
|
29
|
+
if (!p.startsWith(prefix)) return p;
|
|
30
|
+
const [, index, rest] = /^(\d+)(.*)$/s.exec(p.slice(prefix.length)) ?? [];
|
|
31
|
+
const moved = order.indexOf(Number(index));
|
|
32
|
+
return moved < 0 ? void 0 : prefix + moved + rest;
|
|
33
|
+
};
|
|
34
|
+
const records = [...i.r.values()];
|
|
35
|
+
i.r.clear();
|
|
36
|
+
for (const rec of records) {
|
|
37
|
+
const p = rename(rec.p);
|
|
38
|
+
if (p === void 0) continue;
|
|
39
|
+
if (p !== rec.p) {
|
|
40
|
+
rec.p = p;
|
|
41
|
+
rec.ver++;
|
|
42
|
+
}
|
|
43
|
+
i.r.set(p, rec);
|
|
44
|
+
}
|
|
45
|
+
const dirty = [...i.dp];
|
|
46
|
+
i.dp.clear();
|
|
47
|
+
for (const p of dirty) {
|
|
48
|
+
const renamed = rename(p);
|
|
49
|
+
if (renamed !== void 0) i.dp.add(renamed);
|
|
50
|
+
}
|
|
51
|
+
i.dirty(path);
|
|
52
|
+
i.ord = i.ord.map(rename).filter((p) => p !== void 0);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Applies `change` to the array (with `item`), to its keys (with a new key)
|
|
56
|
+
* and to the old indexes, which then say where each record moves: one
|
|
57
|
+
* rule for the four edits rather than an index formula per edit.
|
|
58
|
+
*/
|
|
59
|
+
function edit(i, path, item, change) {
|
|
60
|
+
const list = [...get(i.v, path) ?? []];
|
|
61
|
+
const order = list.map((_, index) => index);
|
|
62
|
+
change(list, item);
|
|
63
|
+
change(order, -1);
|
|
64
|
+
change(keysOf(i, path), `k${++next}`);
|
|
65
|
+
i.v = set(i.v, path, list);
|
|
66
|
+
remap(i, path, order);
|
|
67
|
+
i.all();
|
|
68
|
+
}
|
|
69
|
+
var ArrayFieldImpl = class extends FieldViewImpl {
|
|
70
|
+
/** The form view, for its items' cached views, and the item's shape. */
|
|
71
|
+
f = void 0;
|
|
72
|
+
s = {};
|
|
73
|
+
get keys() {
|
|
74
|
+
this.$.read(this.r.p, 64);
|
|
75
|
+
return keysOf(this.$.i, this.r.p);
|
|
76
|
+
}
|
|
77
|
+
get items() {
|
|
78
|
+
return this.keys.map((_, n) => at(this.f, `${this.r.p}.${n}`, this.s));
|
|
79
|
+
}
|
|
80
|
+
append(item) {
|
|
81
|
+
edit(this.$.i, this.r.p, item, (list, it) => void list.push(it));
|
|
82
|
+
}
|
|
83
|
+
insert(index, item) {
|
|
84
|
+
edit(this.$.i, this.r.p, item, (list, it) => void list.splice(index, 0, it));
|
|
85
|
+
}
|
|
86
|
+
remove(index) {
|
|
87
|
+
edit(this.$.i, this.r.p, void 0, (list) => void list.splice(index, 1));
|
|
88
|
+
}
|
|
89
|
+
move(from, to) {
|
|
90
|
+
edit(this.$.i, this.r.p, void 0, (list) => void list.splice(to, 0, ...list.splice(from, 1)));
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
/** The field of the array at `path` (`Meta.a`): a field, and its items. */
|
|
94
|
+
function arrayField(form, path, item) {
|
|
95
|
+
const field = fieldAt(form, path, ArrayFieldImpl);
|
|
96
|
+
field.f = form;
|
|
97
|
+
field.s = item;
|
|
98
|
+
return field;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
export { arrayField };
|
package/dist/checks.d.ts
ADDED
package/dist/checks.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
//#region src/checks/index.ts
|
|
2
|
+
const DEBOUNCE = 300;
|
|
3
|
+
function start(i, path, run) {
|
|
4
|
+
run.t = void 0;
|
|
5
|
+
const controller = run.c = new AbortController();
|
|
6
|
+
const rec = i.rec(path);
|
|
7
|
+
const done = (message) => {
|
|
8
|
+
if (controller.signal.aborted) return;
|
|
9
|
+
run.c = void 0;
|
|
10
|
+
rec.vd--;
|
|
11
|
+
const changed = i.error_(rec, typeof message === "string" ? message : void 0, 1);
|
|
12
|
+
i.tell([path], 16 | (changed ? 34 : 0), changed ? 4 : 0);
|
|
13
|
+
};
|
|
14
|
+
run.p = Promise.resolve().then(() => run.k(run.v, { signal: controller.signal })).then(done, () => done(void 0));
|
|
15
|
+
}
|
|
16
|
+
const checks = {
|
|
17
|
+
run(instance, path, value) {
|
|
18
|
+
const i = instance;
|
|
19
|
+
const check = i.F.o.checks?.[path];
|
|
20
|
+
if (!check) return;
|
|
21
|
+
const all = i.ck ??= /* @__PURE__ */ new Map();
|
|
22
|
+
let run = all.get(path);
|
|
23
|
+
if (!run) all.set(path, run = { k: check });
|
|
24
|
+
if (!run.t && !run.c) {
|
|
25
|
+
i.rec(path).vd++;
|
|
26
|
+
i.tell([path], 16, 0);
|
|
27
|
+
}
|
|
28
|
+
clearTimeout(run.t);
|
|
29
|
+
run.c?.abort();
|
|
30
|
+
run.v = value;
|
|
31
|
+
const due = run;
|
|
32
|
+
run.t = setTimeout(() => start(i, path, due), DEBOUNCE);
|
|
33
|
+
},
|
|
34
|
+
async settle(instance) {
|
|
35
|
+
const waiting = [];
|
|
36
|
+
for (const [path, run] of instance.ck ?? []) {
|
|
37
|
+
if (run.t !== void 0) {
|
|
38
|
+
clearTimeout(run.t);
|
|
39
|
+
start(instance, path, run);
|
|
40
|
+
}
|
|
41
|
+
if (run.c) waiting.push(run.p);
|
|
42
|
+
}
|
|
43
|
+
await Promise.all(waiting);
|
|
44
|
+
},
|
|
45
|
+
stop(instance) {
|
|
46
|
+
for (const run of instance.ck?.values() ?? []) {
|
|
47
|
+
clearTimeout(run.t);
|
|
48
|
+
run.c?.abort();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
//#endregion
|
|
53
|
+
export { checks };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { a as Meta, i as Form } from "../view-5_kyMWP3.js";
|
|
2
|
+
import { d as FormOptions } from "../types-CdCFdlFp.js";
|
|
3
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
4
|
+
//#region src/factories/fn.d.ts
|
|
5
|
+
type Submit = ConstructorParameters<typeof Form>[0];
|
|
6
|
+
export declare const fnForm: (schema: StandardSchemaV1, submit: Submit, options?: FormOptions<never>, meta?: Meta) => Form;
|
|
7
|
+
//#endregion
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { a as Meta, i as Form } from "../view-5_kyMWP3.js";
|
|
2
|
+
import { d as FormOptions } from "../types-CdCFdlFp.js";
|
|
3
|
+
//#region src/factories/route.d.ts
|
|
4
|
+
export declare const routeForm: (route: string, options?: FormOptions<never>, meta?: Meta) => Form;
|
|
5
|
+
//#endregion
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { t as Form } from "../form-BZ2OH06z.js";
|
|
2
|
+
import * as query from "@menapo/query";
|
|
3
|
+
//#region src/route/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Route forms (D2): `form("PATCH /projects/:id")` submits through
|
|
6
|
+
* `@menapo/query`'s `mutation()`, so the convention's refresh, the
|
|
7
|
+
* optimistic journal, upload progress, `ApiError` and the request's client
|
|
8
|
+
* on a server (`withClient`) are query's, not a second copy here. The raw
|
|
9
|
+
* input is sent; the server validates it (D10). Only route forms import
|
|
10
|
+
* this module, so a function form ships nothing of query (D17).
|
|
11
|
+
*/
|
|
12
|
+
/** Makes the definition's mutation, with `optimistic` and `refresh` passed through unchanged. */
|
|
13
|
+
function routeInit(form) {
|
|
14
|
+
const { optimistic, refresh } = form.o;
|
|
15
|
+
form.m = query.mutation(form.r, {
|
|
16
|
+
...optimistic && { optimistic },
|
|
17
|
+
...refresh && { refresh }
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const routeKind = (form, input, _output, call) => form.m(input, call);
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/factories/route.ts
|
|
23
|
+
/**
|
|
24
|
+
* The route form's factory (D17): what the plugin points a top-level
|
|
25
|
+
* `form("METHOD /path")` at, with its hidden last argument (the name, the
|
|
26
|
+
* operation's browser schema, D4, and the field spec, D13). Internal:
|
|
27
|
+
* resolved by file, never a package entry.
|
|
28
|
+
*/
|
|
29
|
+
const routeForm = (route, options, meta) => new Form(route, void 0, options, meta, routeKind, routeInit);
|
|
30
|
+
//#endregion
|
|
31
|
+
export { routeForm };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { i as fail } from "./instance-C1aijv06.js";
|
|
2
|
+
import { arrayField } from "./array.js";
|
|
3
|
+
import { checks } from "./checks.js";
|
|
4
|
+
//#region src/core/form.ts
|
|
5
|
+
var Form = class {
|
|
6
|
+
/** The name, for DevTools and errors. */
|
|
7
|
+
n;
|
|
8
|
+
s;
|
|
9
|
+
f;
|
|
10
|
+
o;
|
|
11
|
+
c;
|
|
12
|
+
a;
|
|
13
|
+
x;
|
|
14
|
+
/** A route form's route, or a function form's submit. */
|
|
15
|
+
r;
|
|
16
|
+
/** The input's shape, for `fields`; each instance's values extend it (view.ts). */
|
|
17
|
+
sh = void 0;
|
|
18
|
+
/** A route form's mutation, made by its kind. */
|
|
19
|
+
m = void 0;
|
|
20
|
+
constructor(source, schema, options, meta, kind, init) {
|
|
21
|
+
this.r = source;
|
|
22
|
+
this.s = meta?.s ?? schema;
|
|
23
|
+
this.f = meta?.f ?? {};
|
|
24
|
+
this.o = options ?? {};
|
|
25
|
+
this.c = meta?.c;
|
|
26
|
+
this.a = meta?.a;
|
|
27
|
+
this.x = kind;
|
|
28
|
+
this.n = meta?.n ?? (typeof source === "string" ? `form("${source}")` : "form(schema, submit)");
|
|
29
|
+
init?.(this);
|
|
30
|
+
}
|
|
31
|
+
get name() {
|
|
32
|
+
return this.n;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
/** A function form's kind: `submit(output, { signal })` (D10). */
|
|
36
|
+
const fnKind = async (form, _input, output, call) => form.r(output, { signal: call.signal });
|
|
37
|
+
/** A route form without the plugin: its kind lives in `route/`, which only the plugin points it at. */
|
|
38
|
+
const noRoute = (form) => Promise.reject(fail("NO_PLUGIN", `${form.n} submits through @menapo/query, which the Vite plugin wires; add forms() from "@menapo/forms/vite" to vite.config.ts.`));
|
|
39
|
+
function form(a, b, c) {
|
|
40
|
+
const options = typeof a === "string" ? b : c;
|
|
41
|
+
const meta = options?.checks ? {
|
|
42
|
+
a: arrayField,
|
|
43
|
+
c: checks
|
|
44
|
+
} : { a: arrayField };
|
|
45
|
+
return typeof a === "string" ? new Form(a, void 0, options, meta, noRoute) : new Form(b, a, options, meta, fnKind);
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { fnKind as n, form as r, Form as t };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { a as objectOptions, r as create } from "../view--JGPLHEo.js";
|
|
2
|
+
import { n as jsx, t as names } from "../names-D_TE3BpM.js";
|
|
3
|
+
import { n as same } from "../source-DdFTPjID.js";
|
|
4
|
+
import { useId, useLayoutEffect, useState } from "preact/hooks";
|
|
5
|
+
//#region ../glue/src/preact.ts
|
|
6
|
+
/**
|
|
7
|
+
* Preact (machine PLAN §5.5): React's shape on `preact/hooks`, which has no
|
|
8
|
+
* `useSyncExternalStore`: a state update when the snapshot moved, checked
|
|
9
|
+
* again once subscribed so a change between render and subscription is not
|
|
10
|
+
* lost. Side effects start in a layout effect, props are taken after each
|
|
11
|
+
* commit.
|
|
12
|
+
*/
|
|
13
|
+
function useSource(create, props) {
|
|
14
|
+
const id = useId();
|
|
15
|
+
const [binding] = useState(() => ({
|
|
16
|
+
source: create(id, props),
|
|
17
|
+
seen: 0,
|
|
18
|
+
props
|
|
19
|
+
}));
|
|
20
|
+
const [, render] = useState(0);
|
|
21
|
+
const { source } = binding;
|
|
22
|
+
binding.seen = source.snapshot();
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const check = () => {
|
|
25
|
+
const now = source.snapshot();
|
|
26
|
+
if (now !== binding.seen) render(now);
|
|
27
|
+
};
|
|
28
|
+
const off = source.subscribe(check);
|
|
29
|
+
source.mount();
|
|
30
|
+
check();
|
|
31
|
+
return () => {
|
|
32
|
+
off();
|
|
33
|
+
source.unmount();
|
|
34
|
+
};
|
|
35
|
+
}, [source]);
|
|
36
|
+
useLayoutEffect(() => {
|
|
37
|
+
if (props && !same(props, binding.props)) source.setProps(binding.props = props);
|
|
38
|
+
source.commit();
|
|
39
|
+
});
|
|
40
|
+
return source;
|
|
41
|
+
}
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/glue/preact.ts
|
|
44
|
+
/**
|
|
45
|
+
* `useForm` in Preact (PLAN §5.2): the shared glue's hooks
|
|
46
|
+
* adapter. Reads are tracked again on each render, so the component renders
|
|
47
|
+
* only when something it read changed; the view object is new per version
|
|
48
|
+
* (as in React). Options are taken after each
|
|
49
|
+
* commit, so a render that never commits changes nothing.
|
|
50
|
+
*/
|
|
51
|
+
const N = names(jsx, false, true);
|
|
52
|
+
function useForm(source, options) {
|
|
53
|
+
objectOptions(options, "Preact");
|
|
54
|
+
const view = useSource(create(source, N), options);
|
|
55
|
+
view.restart();
|
|
56
|
+
return view.current();
|
|
57
|
+
}
|
|
58
|
+
//#endregion
|
|
59
|
+
export { useForm };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { a as objectOptions, r as create } from "../view--JGPLHEo.js";
|
|
2
|
+
import { i as react, t as names } from "../names-D_TE3BpM.js";
|
|
3
|
+
import { n as same } from "../source-DdFTPjID.js";
|
|
4
|
+
import { useId, useLayoutEffect, useState, useSyncExternalStore } from "react";
|
|
5
|
+
//#region ../glue/src/react.ts
|
|
6
|
+
/**
|
|
7
|
+
* React (machine PLAN §5.5, spike S3): the source lives in `useState`, so a
|
|
8
|
+
* render React throws away (StrictMode, a concurrent render) never mutates
|
|
9
|
+
* a live one; `useSyncExternalStore` renders once per snapshot move and
|
|
10
|
+
* never tears; side effects start in a layout effect, after the DOM is
|
|
11
|
+
* committed, and stop on unmount (StrictMode's mount, unmount and mount
|
|
12
|
+
* leave one running). Props are taken after each commit, so a render that
|
|
13
|
+
* never commits never changes the source. No `flushSync`.
|
|
14
|
+
*/
|
|
15
|
+
function useSource(create, props) {
|
|
16
|
+
const id = useId();
|
|
17
|
+
const [binding] = useState(() => {
|
|
18
|
+
const source = create(id, props);
|
|
19
|
+
return {
|
|
20
|
+
source,
|
|
21
|
+
subscribe: (listener) => source.subscribe(listener),
|
|
22
|
+
snapshot: () => source.snapshot(),
|
|
23
|
+
props
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
const { source } = binding;
|
|
27
|
+
useSyncExternalStore(binding.subscribe, binding.snapshot, binding.snapshot);
|
|
28
|
+
useLayoutEffect(() => {
|
|
29
|
+
source.mount();
|
|
30
|
+
return () => source.unmount();
|
|
31
|
+
}, [source]);
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
if (props && !same(props, binding.props)) source.setProps(binding.props = props);
|
|
34
|
+
source.commit();
|
|
35
|
+
});
|
|
36
|
+
return source;
|
|
37
|
+
}
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/glue/react.ts
|
|
40
|
+
/**
|
|
41
|
+
* `useForm` in React (PLAN §5.2): the shared glue's `useSyncExternalStore`
|
|
42
|
+
* adapter. Reads are tracked again on each render, so the component renders
|
|
43
|
+
* only when something it read changed; the view object is new per version
|
|
44
|
+
* (React's Compiler compares by identity). Options are taken after each
|
|
45
|
+
* commit, so a render that never commits changes nothing.
|
|
46
|
+
*/
|
|
47
|
+
const N = names(react, true);
|
|
48
|
+
function useForm(source, options) {
|
|
49
|
+
objectOptions(options, "React");
|
|
50
|
+
const view = useSource(create(source, N), options);
|
|
51
|
+
view.restart();
|
|
52
|
+
return view.current();
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { useForm };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { r as create } from "../view--JGPLHEo.js";
|
|
2
|
+
import { n as jsx, t as names } from "../names-D_TE3BpM.js";
|
|
3
|
+
import { createComputed, createSignal, createUniqueId, onCleanup, onMount, untrack } from "solid-js";
|
|
4
|
+
//#region ../glue/src/solid.ts
|
|
5
|
+
/**
|
|
6
|
+
* Solid (machine PLAN §5.5): a signal holding the snapshot, read through
|
|
7
|
+
* the returned tracker, so only the computations that read it re-run, and
|
|
8
|
+
* Solid's own spread applies only the attributes that changed. Props given
|
|
9
|
+
* as a function are read in a computation; side effects start in `onMount`
|
|
10
|
+
* and run after each change, once Solid has updated the DOM.
|
|
11
|
+
*/
|
|
12
|
+
function useSource(create, props) {
|
|
13
|
+
const read = typeof props === "function" ? props : void 0;
|
|
14
|
+
const source = create(createUniqueId(), read ? untrack(read) : props);
|
|
15
|
+
const [version, set] = createSignal(source.snapshot());
|
|
16
|
+
const commit = () => source.commit();
|
|
17
|
+
const off = source.subscribe(() => {
|
|
18
|
+
set(source.snapshot());
|
|
19
|
+
queueMicrotask(commit);
|
|
20
|
+
});
|
|
21
|
+
if (read) createComputed(() => {
|
|
22
|
+
const next = read();
|
|
23
|
+
untrack(() => source.setProps(next));
|
|
24
|
+
});
|
|
25
|
+
onMount(() => source.mount());
|
|
26
|
+
onCleanup(() => {
|
|
27
|
+
off();
|
|
28
|
+
source.unmount();
|
|
29
|
+
});
|
|
30
|
+
return [source, version];
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/glue/solid.ts
|
|
34
|
+
/**
|
|
35
|
+
* `useForm` in Solid (PLAN §5.2): the shared glue's signal adapter;
|
|
36
|
+
* every read calls the tracker, and options given as a getter are watched,
|
|
37
|
+
* so pristine fields follow new defaults (D11). Handlers are named
|
|
38
|
+
* `onInput`, `onFocusOut`, which Solid lowercases.
|
|
39
|
+
*/
|
|
40
|
+
const N = names(jsx, false, true);
|
|
41
|
+
function useForm(source, options) {
|
|
42
|
+
const [view, track] = useSource(create(source, N), options);
|
|
43
|
+
view.k = track;
|
|
44
|
+
view.keep = true;
|
|
45
|
+
return view.current();
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
48
|
+
export { useForm };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { r as create } from "../view--JGPLHEo.js";
|
|
2
|
+
import { r as lower, t as names } from "../names-D_TE3BpM.js";
|
|
3
|
+
import { t as nextId } from "../source-DdFTPjID.js";
|
|
4
|
+
import { onMount, tick, untrack } from "svelte";
|
|
5
|
+
import { createSubscriber } from "svelte/reactivity";
|
|
6
|
+
//#region ../glue/src/svelte.svelte.ts
|
|
7
|
+
/**
|
|
8
|
+
* Svelte 5 (machine PLAN §5.5): `createSubscriber` re-runs the effects and
|
|
9
|
+
* deriveds that read the tracker when the snapshot moves; props given as a
|
|
10
|
+
* getter are taken in `$effect.pre`, before the DOM updates. Side effects
|
|
11
|
+
* start in `onMount` and run after each update that follows a change.
|
|
12
|
+
* Svelte gives a module no id of its own, so ids come from a counter (or
|
|
13
|
+
* the owner's `id`, which SSR needs).
|
|
14
|
+
*/
|
|
15
|
+
function useSource(create, props) {
|
|
16
|
+
const read = typeof props === "function" ? props : void 0;
|
|
17
|
+
const source = create(nextId(), read ? read() : props);
|
|
18
|
+
let update;
|
|
19
|
+
let seen = source.snapshot();
|
|
20
|
+
const track = createSubscriber((u) => {
|
|
21
|
+
update = u;
|
|
22
|
+
return () => update = void 0;
|
|
23
|
+
});
|
|
24
|
+
const commit = () => source.commit();
|
|
25
|
+
const off = source.subscribe(() => {
|
|
26
|
+
const now = source.snapshot();
|
|
27
|
+
if (now !== seen) {
|
|
28
|
+
seen = now;
|
|
29
|
+
update?.();
|
|
30
|
+
}
|
|
31
|
+
tick().then(commit);
|
|
32
|
+
});
|
|
33
|
+
if (read) $effect.pre(() => {
|
|
34
|
+
const next = read();
|
|
35
|
+
untrack(() => source.setProps(next));
|
|
36
|
+
});
|
|
37
|
+
onMount(() => {
|
|
38
|
+
source.mount();
|
|
39
|
+
return () => {
|
|
40
|
+
off();
|
|
41
|
+
source.unmount();
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
return [source, track];
|
|
45
|
+
}
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/glue/svelte.svelte.ts
|
|
48
|
+
/**
|
|
49
|
+
* `useForm` in Svelte 5 (PLAN §5.2): the shared glue's `createSubscriber` adapter;
|
|
50
|
+
* every read calls the tracker, and options given as a getter are watched,
|
|
51
|
+
* so pristine fields follow new defaults (D11). Handlers are named
|
|
52
|
+
* `oninput`, `onfocusout` (glue's naming).
|
|
53
|
+
*/
|
|
54
|
+
const N = names(lower, false);
|
|
55
|
+
function useForm(source, options) {
|
|
56
|
+
const [view, track] = useSource(create(source, N), options);
|
|
57
|
+
view.k = track;
|
|
58
|
+
view.keep = true;
|
|
59
|
+
return view.current();
|
|
60
|
+
}
|
|
61
|
+
//#endregion
|
|
62
|
+
export { useForm };
|