jsonisch 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +218 -0
- package/dist/form-ref-BEri3JKv.js +1123 -0
- package/dist/form-ref-D_xHSaqL.d.ts +994 -0
- package/dist/handle-submit-CD3WY9gW.js +208 -0
- package/dist/handle-submit-Dk7QW3_q.d.ts +34 -0
- package/dist/index.d.ts +724 -0
- package/dist/index.js +1521 -0
- package/dist/plugin/index.d.ts +188 -0
- package/dist/plugin/index.js +4 -0
- package/dist/plugin-BCsAE8RK.d.ts +612 -0
- package/dist/plugin-Qka-vaO1.js +1545 -0
- package/dist/react/index.d.ts +476 -0
- package/dist/react/index.js +456 -0
- package/docs/decode-fork.md +24 -0
- package/docs/plugin-lifecycle.md +44 -0
- package/docs/rerender-history.md +61 -0
- package/docs/wire-shapes.md +70 -0
- package/package.json +87 -0
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrew Brown
|
|
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
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# jsonisch
|
|
2
|
+
|
|
3
|
+
> Schemas as values. A form library for apps where JSON-Schemas are **runtime
|
|
4
|
+
> data** — stored in a database, customized by admins, composed on the fly —
|
|
5
|
+
> and the whole form is derived from the schema value: state, validation,
|
|
6
|
+
> derived values, visibility, dirty-tracking.
|
|
7
|
+
|
|
8
|
+
**Status: experimental.** The API is in production in one app. It is not frozen.
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
pnpm add jsonisch
|
|
12
|
+
# npm install jsonisch
|
|
13
|
+
# yarn add jsonisch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { createFormStore } from "jsonisch";
|
|
18
|
+
import { createFormHook } from "jsonisch/react";
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
React is an optional peer. The core store is DOM-free.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## The problem: schemas as values
|
|
26
|
+
|
|
27
|
+
Most form libraries assume the shape of your form is known at build time —
|
|
28
|
+
a Zod schema in a module, types inferred from it, a hand-written component
|
|
29
|
+
per field. That assumption breaks the moment your app lets users customize
|
|
30
|
+
their forms: now the schema is a **value**, fetched from a database at
|
|
31
|
+
request time, different per tenant, edited without a deploy. There is no
|
|
32
|
+
compile-time type to infer against, and nobody is hand-writing a component
|
|
33
|
+
per field for a form that didn't exist yesterday.
|
|
34
|
+
|
|
35
|
+
`jsonisch` starts from that world. It takes a JSON-Schema value, walks it
|
|
36
|
+
once, and gives you a fully reactive, validated form with the fields already
|
|
37
|
+
wired:
|
|
38
|
+
|
|
39
|
+
- **Validation** through an injected validator — the interface is
|
|
40
|
+
deliberately AJV-shaped, so a compiled AJV validate function passes
|
|
41
|
+
through unchanged (a first-party validation library may follow).
|
|
42
|
+
- **Rendering** through YOUR components — shadcn, your design system,
|
|
43
|
+
anything. You register widgets once, keyed by control kind; every schema
|
|
44
|
+
a tenant can invent renders through that one registry.
|
|
45
|
+
- **Derived values** through an injected calc engine that owns the
|
|
46
|
+
expression language; jsonisch owns the scope construction and the
|
|
47
|
+
derivation wiring (dependency graph, computed signals, exclusion from
|
|
48
|
+
dirty-tracking and the submit payload *by construction*).
|
|
49
|
+
- **Dirty-tracking, visibility, reset, submit** — driven by the schema
|
|
50
|
+
walk, not by per-field wiring.
|
|
51
|
+
|
|
52
|
+
You bring the components; it brings everything else.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Quickstart
|
|
57
|
+
|
|
58
|
+
Write your widgets as plain controlled components — here with shadcn:
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
// widgets.tsx
|
|
62
|
+
import { Input } from "@/components/ui/input";
|
|
63
|
+
import { Label } from "@/components/ui/label";
|
|
64
|
+
import type { WidgetProps } from "jsonisch/react";
|
|
65
|
+
|
|
66
|
+
export function TextWidget({ field }: WidgetProps) {
|
|
67
|
+
return (
|
|
68
|
+
<div>
|
|
69
|
+
<Label htmlFor={field.name}>{field.schema.title ?? field.name}</Label>
|
|
70
|
+
<Input
|
|
71
|
+
id={field.name}
|
|
72
|
+
value={(field.input as string) ?? ""}
|
|
73
|
+
onChange={(e) => field.onChange(e.target.value)}
|
|
74
|
+
{...field.props}
|
|
75
|
+
/>
|
|
76
|
+
{field.errors && <p className="text-sm text-destructive">{field.errors[0]}</p>}
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function CurrencyWidget({ field }: WidgetProps) {
|
|
82
|
+
/* same shape: field.input in, field.onChange out */
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function SelectWidget({ field }: WidgetProps) {
|
|
86
|
+
/* options come from field.schema.enum — the schema node rides along */
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Register them once, with a validator, at module level:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
// form.ts
|
|
94
|
+
import Ajv from "ajv";
|
|
95
|
+
import { createFormHook } from "jsonisch/react";
|
|
96
|
+
import { CurrencyWidget, SelectWidget, TextWidget } from "./widgets";
|
|
97
|
+
|
|
98
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
99
|
+
|
|
100
|
+
export const { useAppForm, Form, Field } = createFormHook({
|
|
101
|
+
widgets: { text: TextWidget, currency: CurrencyWidget, select: SelectWidget },
|
|
102
|
+
validate: (schema) => {
|
|
103
|
+
const check = ajv.compile(schema);
|
|
104
|
+
return (input) => (check(input) ? null : check.errors);
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Then every form is two lines, no matter what schema shows up:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
// Fetched from your database — a VALUE, not a type
|
|
113
|
+
const schema = {
|
|
114
|
+
type: "object",
|
|
115
|
+
required: ["borrowerName"],
|
|
116
|
+
properties: {
|
|
117
|
+
borrowerName: { type: "string", title: "Borrower name" },
|
|
118
|
+
loanAmount: {
|
|
119
|
+
type: "number",
|
|
120
|
+
title: "Loan amount",
|
|
121
|
+
"x-ui": { control: "currency" },
|
|
122
|
+
},
|
|
123
|
+
loanType: {
|
|
124
|
+
type: "string",
|
|
125
|
+
title: "Loan type",
|
|
126
|
+
enum: ["bridge", "construction", "rental"],
|
|
127
|
+
"x-ui": { control: "select" },
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
function LoanForm({ record }: { record: unknown }) {
|
|
133
|
+
const form = useAppForm({ schema, initialInput: record });
|
|
134
|
+
return <Form of={form} onSubmit={(output) => save(output)} />;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
`<Form>` without children renders the whole form from the schema through the
|
|
139
|
+
widget registry — no hand-written field components. `onSubmit` receives the
|
|
140
|
+
validated output; an invalid submit blocks the handler and focuses the first
|
|
141
|
+
erroring field. For custom layouts, `<Field of={form} path={["loanAmount"]} />`
|
|
142
|
+
places one registry-dispatched field, and a render-function child makes it
|
|
143
|
+
headless.
|
|
144
|
+
|
|
145
|
+
Widgets resolve by **control kind** — an explicit `x-ui.control` on the
|
|
146
|
+
schema node, or inferred from `format`/`type` (`inferControl` is exported).
|
|
147
|
+
A kind without a registry entry renders a visible fallback naming the
|
|
148
|
+
missing kind, so a schema misconfiguration can't silently drop a field.
|
|
149
|
+
|
|
150
|
+
Derived fields declare a formula on the schema node (`x-formula`), and the
|
|
151
|
+
form evaluates them reactively through a `CalcEngine` you inject as a
|
|
152
|
+
plugin — parse, evaluate, extract dependencies. The engine owns the
|
|
153
|
+
expression language; jsonisch builds the eval scopes (including the
|
|
154
|
+
root-record alias — `rootRecordAlias`, default `"record"` — the key stored
|
|
155
|
+
formulas use to address the root record) and wires the dependency graph.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Where jsonisch sits — a deliberate "best of three"
|
|
160
|
+
|
|
161
|
+
`jsonisch` is a synthesis of three lineages, picking one idea from each:
|
|
162
|
+
|
|
163
|
+
- **From TanStack Form** — the **framework-agnostic core + thin adapters**
|
|
164
|
+
layout, and the **`createFormHook({ widgets })` composition/registry**
|
|
165
|
+
pattern: register your design-system widgets once, get a typed
|
|
166
|
+
`useAppForm` with them baked in.
|
|
167
|
+
- **From Formisch** — **signals** as the reactivity engine (its own, no
|
|
168
|
+
external signal lib), so a keystroke re-renders only the fields that
|
|
169
|
+
depend on it, and derived fields recompute automatically.
|
|
170
|
+
- **New here** — the schema kind is **JSON-Schema**, not Zod/Valibot/Yup.
|
|
171
|
+
Validation runs through an injected AJV-shaped validator. Derived values
|
|
172
|
+
are computed signals over the formulas already declared in the schema, so
|
|
173
|
+
they're excluded from dirty-tracking and the submit payload *by
|
|
174
|
+
construction*.
|
|
175
|
+
|
|
176
|
+
The re-render question — "when I type one character into a 60-field form,
|
|
177
|
+
what re-renders?" — has a two-decade history that signals largely closed,
|
|
178
|
+
and jsonisch inherits that answer from the formisch lineage rather than
|
|
179
|
+
contributing one. The full story, table and all, lives in
|
|
180
|
+
[docs/rerender-history.md](https://github.com/andrew310/jsonisch/blob/main/docs/rerender-history.md).
|
|
181
|
+
|
|
182
|
+
### Design stance on types
|
|
183
|
+
|
|
184
|
+
TanStack Form's headline is deep, compile-time **path type-inference**.
|
|
185
|
+
`jsonisch` deliberately **drops it** — our schemas are runtime database
|
|
186
|
+
data, so field paths are runtime values with no compile-time shape to infer
|
|
187
|
+
against. The bet: keep *enough* typing that `tsc` stays a cheap,
|
|
188
|
+
deterministic tripwire on the code (still very much worth it), but skip the
|
|
189
|
+
galaxy-brain generics, and lean on **AJV + tests** for the runtime-shape
|
|
190
|
+
correctness that types can't cover for a schema that only exists at runtime
|
|
191
|
+
anyway.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Architecture
|
|
196
|
+
|
|
197
|
+
- **core** — framework-agnostic store. `createFormStore(config, deps)` walks
|
|
198
|
+
the JSON-Schema once and builds a field-store tree
|
|
199
|
+
(`kind: array | object | value`), each node carrying signals
|
|
200
|
+
(`input`/`initialInput` for dirty-vs-reset, `errors`, `isDirty`, DOM
|
|
201
|
+
`elements`). DOM-free and isomorphic — the same walk runs server-side.
|
|
202
|
+
- **methods** — tree-shakeable ops: `setInput`, `validate`, `reset`,
|
|
203
|
+
`insert/move/remove/swap`, `handleSubmit`, `pickDirty`, `applyBaseline`.
|
|
204
|
+
- **react** — `createFormHook`, `useAppForm`, `<Form>`, headless `<Field>`.
|
|
205
|
+
|
|
206
|
+
Deeper dives, with diagrams, live in
|
|
207
|
+
[`docs/`](https://github.com/andrew310/jsonisch/tree/main/docs):
|
|
208
|
+
|
|
209
|
+
- [rerender-history.md](https://github.com/andrew310/jsonisch/blob/main/docs/rerender-history.md) — a short history of the form re-render problem, and what a signal is, concretely.
|
|
210
|
+
- [decode-fork.md](https://github.com/andrew310/jsonisch/blob/main/docs/decode-fork.md) — how a server record becomes `initialInput` (`x-column` routing, the envelope twin).
|
|
211
|
+
- [plugin-lifecycle.md](https://github.com/andrew310/jsonisch/blob/main/docs/plugin-lifecycle.md) — the pass order at build, and the reseed / rebase / reset / transfer sites.
|
|
212
|
+
- [wire-shapes.md](https://github.com/andrew310/jsonisch/blob/main/docs/wire-shapes.md) — the save payload partition, the persisted envelopes, and the LOS-461 skip policy.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## License / status
|
|
217
|
+
|
|
218
|
+
MIT. Nothing here is API-stable yet.
|