schema-components 1.10.4 → 1.10.5
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/package.json +1 -1
- package/README.md +0 -727
package/package.json
CHANGED
package/README.md
DELETED
|
@@ -1,727 +0,0 @@
|
|
|
1
|
-
# schema-components
|
|
2
|
-
|
|
3
|
-
React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents.
|
|
4
|
-
|
|
5
|
-
Define your data model once. Get presentational views, input fields, and editable forms — no manual wiring.
|
|
6
|
-
|
|
7
|
-
## Install
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install schema-components
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Peer dependencies: `zod@^4.0.0`, `react@^18.0.0 || ^19.0.0`.
|
|
14
|
-
|
|
15
|
-
## Quick start
|
|
16
|
-
|
|
17
|
-
```tsx
|
|
18
|
-
import { z } from "zod";
|
|
19
|
-
import { SchemaComponent } from "schema-components/react/SchemaComponent";
|
|
20
|
-
|
|
21
|
-
const userSchema = z.object({
|
|
22
|
-
name: z.string().min(1).meta({ description: "Full name" }),
|
|
23
|
-
email: z.email().meta({ description: "Email address" }),
|
|
24
|
-
role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
|
|
25
|
-
active: z.boolean().meta({ description: "Active" }),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
function UserCard() {
|
|
29
|
-
const [user, setUser] = useState({
|
|
30
|
-
name: "Ada Lovelace",
|
|
31
|
-
email: "ada@example.com",
|
|
32
|
-
role: "admin",
|
|
33
|
-
active: true,
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
return (
|
|
37
|
-
<SchemaComponent
|
|
38
|
-
schema={userSchema}
|
|
39
|
-
value={user}
|
|
40
|
-
onChange={setUser}
|
|
41
|
-
/>
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Renders every field as an editable input. Add `readOnly` to the component for a read-only view:
|
|
47
|
-
|
|
48
|
-
```tsx
|
|
49
|
-
<SchemaComponent schema={userSchema} value={user} readOnly />
|
|
50
|
-
```
|
|
51
|
-
|
|
52
|
-
## How it works
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
Zod schema ─── z.toJSONSchema() ──→ JSON Schema ──────────┐
|
|
56
|
-
▼
|
|
57
|
-
JSON Schema ─────────────────────────────────────────► JSON Schema ──► walker ──► React
|
|
58
|
-
▲
|
|
59
|
-
OpenAPI doc ── extract schemas ───────────────────────────┘
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
One walker, one input format. The walker reads standard JSON Schema keywords (Draft 2020-12) — decoupled from Zod's internal API. `z.toJSONSchema()` is lossless: it preserves `readOnly`, `writeOnly`, custom `.meta()` properties, constraints, formats, and defaults.
|
|
63
|
-
|
|
64
|
-
`z.fromJSONSchema()` is used **only for validation** — converting JSON Schema / OpenAPI inputs back to Zod when `validate` is true and the original wasn't a Zod schema.
|
|
65
|
-
|
|
66
|
-
## Component editability
|
|
67
|
-
|
|
68
|
-
Fields render in one of three states, controlled by `readOnly` and `writeOnly` from three sources:
|
|
69
|
-
|
|
70
|
-
| State | Rendering |
|
|
71
|
-
|---|---|
|
|
72
|
-
| **Presentation** | Read-only display. Formatted text, links, badges. No inputs. |
|
|
73
|
-
| **Input** | Empty field. Blank inputs, "Select…" dropdowns, unchecked toggles. |
|
|
74
|
-
| **Editable** | Pre-populated input the user can change. |
|
|
75
|
-
|
|
76
|
-
### Three sources, priority order
|
|
77
|
-
|
|
78
|
-
1. **Schema property** (`.meta({ readOnly: true })`) — always wins
|
|
79
|
-
2. **Component props** (`readOnly` / `writeOnly` on `<SchemaComponent>`) — rendering context
|
|
80
|
-
3. **Schema root** (`.meta({ readOnly: true })` on root schema) — fallback default
|
|
81
|
-
4. Neither → Editable
|
|
82
|
-
|
|
83
|
-
### Overriding with `readOnly: false`
|
|
84
|
-
|
|
85
|
-
A field override can explicitly opt out of a higher-level `readOnly`:
|
|
86
|
-
|
|
87
|
-
```tsx
|
|
88
|
-
<SchemaComponent
|
|
89
|
-
schema={userSchema}
|
|
90
|
-
value={user}
|
|
91
|
-
readOnly // everything presentation
|
|
92
|
-
fields={{
|
|
93
|
-
address: {
|
|
94
|
-
readOnly: false, // address subtree: editable
|
|
95
|
-
city: { readOnly: true }, // city: still presentation
|
|
96
|
-
},
|
|
97
|
-
}}
|
|
98
|
-
/>
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Type-safe field overrides
|
|
102
|
-
|
|
103
|
-
The `fields` prop type is inferred from the schema:
|
|
104
|
-
|
|
105
|
-
```tsx
|
|
106
|
-
// Zod — full autocomplete
|
|
107
|
-
<SchemaComponent
|
|
108
|
-
schema={userSchema}
|
|
109
|
-
fields={{
|
|
110
|
-
name: { readOnly: true }, // ✓ type-safe
|
|
111
|
-
address: {
|
|
112
|
-
city: { description: "City" }, // ✓ nested, type-safe
|
|
113
|
-
},
|
|
114
|
-
// nme: { readOnly: true }, // ✗ TypeScript error: unknown key
|
|
115
|
-
}}
|
|
116
|
-
/>
|
|
117
|
-
|
|
118
|
-
// JSON Schema as const — full autocomplete
|
|
119
|
-
const jsonSchema = {
|
|
120
|
-
type: "object" as const,
|
|
121
|
-
properties: {
|
|
122
|
-
name: { type: "string" as const },
|
|
123
|
-
email: { type: "string" as const, format: "email" },
|
|
124
|
-
},
|
|
125
|
-
required: ["name"],
|
|
126
|
-
} as const;
|
|
127
|
-
|
|
128
|
-
<SchemaComponent
|
|
129
|
-
schema={jsonSchema}
|
|
130
|
-
fields={{
|
|
131
|
-
name: { readOnly: true }, // ✓ inferred from as const
|
|
132
|
-
// nme: { readOnly: true }, // ✗ TypeScript error
|
|
133
|
-
}}
|
|
134
|
-
/>
|
|
135
|
-
|
|
136
|
-
// OpenAPI as const + ref — full autocomplete
|
|
137
|
-
const spec = {
|
|
138
|
-
openapi: "3.1.0",
|
|
139
|
-
components: {
|
|
140
|
-
schemas: {
|
|
141
|
-
User: {
|
|
142
|
-
type: "object" as const,
|
|
143
|
-
properties: {
|
|
144
|
-
id: { type: "string" as const },
|
|
145
|
-
name: { type: "string" as const },
|
|
146
|
-
},
|
|
147
|
-
required: ["id", "name"],
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
},
|
|
151
|
-
} as const;
|
|
152
|
-
|
|
153
|
-
<SchemaComponent
|
|
154
|
-
schema={spec}
|
|
155
|
-
ref="#/components/schemas/User"
|
|
156
|
-
fields={{
|
|
157
|
-
id: { readOnly: true }, // ✓ inferred through ref
|
|
158
|
-
}}
|
|
159
|
-
/>
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Individual fields
|
|
163
|
-
|
|
164
|
-
```tsx
|
|
165
|
-
import { SchemaField } from "schema-components/react/SchemaComponent";
|
|
166
|
-
|
|
167
|
-
// Type-safe path — only valid dot-paths accepted
|
|
168
|
-
<SchemaField
|
|
169
|
-
schema={userSchema}
|
|
170
|
-
path="address.city" // ✓ type-safe
|
|
171
|
-
// path="address.cty" // ✗ TypeScript error
|
|
172
|
-
value={user}
|
|
173
|
-
onChange={setUser}
|
|
174
|
-
/>
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
When the schema is a Zod schema or typed `as const`, only valid dot-paths like `"address.city"` are accepted. Invalid paths trigger TypeScript errors. Runtime schemas accept any string.
|
|
178
|
-
|
|
179
|
-
## All input formats
|
|
180
|
-
|
|
181
|
-
`<SchemaComponent>` auto-detects the input format:
|
|
182
|
-
|
|
183
|
-
```tsx
|
|
184
|
-
// Zod schema
|
|
185
|
-
<SchemaComponent schema={z.object({ name: z.string() })} value={data} />
|
|
186
|
-
|
|
187
|
-
// JSON Schema
|
|
188
|
-
<SchemaComponent
|
|
189
|
-
schema={{ type: "object", properties: { name: { type: "string" } } }}
|
|
190
|
-
value={data}
|
|
191
|
-
/>
|
|
192
|
-
|
|
193
|
-
// OpenAPI document + ref
|
|
194
|
-
<SchemaComponent
|
|
195
|
-
schema={openApiSpec}
|
|
196
|
-
ref="#/components/schemas/User"
|
|
197
|
-
value={data}
|
|
198
|
-
/>
|
|
199
|
-
```
|
|
200
|
-
|
|
201
|
-
## OpenAPI components
|
|
202
|
-
|
|
203
|
-
Render API operations with type-safe field overrides:
|
|
204
|
-
|
|
205
|
-
```tsx
|
|
206
|
-
import { ApiOperation } from "schema-components/openapi/components";
|
|
207
|
-
import type { ApiRequestBodyProps } from "schema-components/openapi/components";
|
|
208
|
-
|
|
209
|
-
const petStore = {
|
|
210
|
-
openapi: "3.1.0",
|
|
211
|
-
paths: {
|
|
212
|
-
"/pets": {
|
|
213
|
-
post: {
|
|
214
|
-
requestBody: {
|
|
215
|
-
content: {
|
|
216
|
-
"application/json": {
|
|
217
|
-
schema: {
|
|
218
|
-
type: "object" as const,
|
|
219
|
-
properties: {
|
|
220
|
-
name: { type: "string" as const },
|
|
221
|
-
tag: { type: "string" as const },
|
|
222
|
-
},
|
|
223
|
-
required: ["name"],
|
|
224
|
-
},
|
|
225
|
-
},
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
responses: { "201": { description: "Created" } },
|
|
229
|
-
},
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
} as const;
|
|
233
|
-
|
|
234
|
-
// Full operation — parameters, request body, responses
|
|
235
|
-
<ApiOperation schema={petStore} path="/pets" method="post" />
|
|
236
|
-
|
|
237
|
-
// Just the request body with type-safe fields
|
|
238
|
-
<ApiRequestBody
|
|
239
|
-
schema={petStore}
|
|
240
|
-
path="/pets"
|
|
241
|
-
method="post"
|
|
242
|
-
fields={{
|
|
243
|
-
name: { description: "Pet name" }, // ✓ inferred from as const
|
|
244
|
-
// nme: { description: "X" }, // ✗ TypeScript error
|
|
245
|
-
}}
|
|
246
|
-
/>
|
|
247
|
-
|
|
248
|
-
// Just parameters with type-safe overrides
|
|
249
|
-
<ApiParameters
|
|
250
|
-
schema={petStore}
|
|
251
|
-
path="/pets"
|
|
252
|
-
method="get"
|
|
253
|
-
overrides={{
|
|
254
|
-
limit: { description: "Max results" }, // ✓ inferred parameter names
|
|
255
|
-
}}
|
|
256
|
-
/>
|
|
257
|
-
|
|
258
|
-
// Response schema
|
|
259
|
-
<ApiResponse schema={petStore} path="/pets" method="get" status="200" />
|
|
260
|
-
```
|
|
261
|
-
|
|
262
|
-
## Theme adapters
|
|
263
|
-
|
|
264
|
-
Headless by default (plain HTML). Wrap with a theme adapter for styled components:
|
|
265
|
-
|
|
266
|
-
```tsx
|
|
267
|
-
import { SchemaProvider } from "schema-components/react/SchemaComponent";
|
|
268
|
-
import { shadcnResolver } from "schema-components/themes/shadcn";
|
|
269
|
-
|
|
270
|
-
<SchemaProvider resolver={shadcnResolver}>
|
|
271
|
-
<SchemaComponent schema={userSchema} value={user} onChange={setUser} />
|
|
272
|
-
</SchemaProvider>
|
|
273
|
-
```
|
|
274
|
-
|
|
275
|
-
Write a custom adapter:
|
|
276
|
-
|
|
277
|
-
```tsx
|
|
278
|
-
import type { RenderProps, ComponentResolver } from "schema-components/core/renderer";
|
|
279
|
-
|
|
280
|
-
const myResolver: ComponentResolver = {
|
|
281
|
-
string: (props: RenderProps) => {
|
|
282
|
-
if (props.readOnly) return <span>{props.value}</span>;
|
|
283
|
-
return <input value={props.value} onChange={(e) => props.onChange(e.target.value)} />;
|
|
284
|
-
},
|
|
285
|
-
object: (props: RenderProps) => {
|
|
286
|
-
// props.renderChild recursively renders each field
|
|
287
|
-
return (
|
|
288
|
-
<div>
|
|
289
|
-
{props.fields && Object.entries(props.fields).map(([key, field]) => (
|
|
290
|
-
<div key={key}>
|
|
291
|
-
<label>{field.meta.description}</label>
|
|
292
|
-
{props.renderChild(field, (props.value as Record<string, unknown>)?.[key], (v) => {
|
|
293
|
-
props.onChange({ ...(props.value as object), [key]: v });
|
|
294
|
-
})}
|
|
295
|
-
</div>
|
|
296
|
-
))}
|
|
297
|
-
</div>
|
|
298
|
-
);
|
|
299
|
-
},
|
|
300
|
-
};
|
|
301
|
-
```
|
|
302
|
-
|
|
303
|
-
Every render function receives `props.renderChild` for recursive rendering — no need to know about the resolver or rendering context.
|
|
304
|
-
|
|
305
|
-
## Raw HTML
|
|
306
|
-
|
|
307
|
-
Render schemas to HTML strings — no React needed. Useful for server-side rendering, email templates, static sites, and non-React environments.
|
|
308
|
-
|
|
309
|
-
```tsx
|
|
310
|
-
import { renderToHtml } from "schema-components/html/renderToHtml";
|
|
311
|
-
|
|
312
|
-
const userSchema = z.object({
|
|
313
|
-
name: z.string().meta({ description: "Name" }),
|
|
314
|
-
email: z.email().meta({ description: "Email" }),
|
|
315
|
-
role: z.enum(["admin", "editor", "viewer"]).meta({ description: "Role" }),
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
// Read-only display
|
|
319
|
-
const html = renderToHtml(userSchema, {
|
|
320
|
-
value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
|
|
321
|
-
readOnly: true,
|
|
322
|
-
});
|
|
323
|
-
// → <dl class="sc-object">
|
|
324
|
-
// <dt class="sc-label">Name</dt><dd class="sc-value"><span class="sc-value">Ada Lovelace</span></dd>
|
|
325
|
-
// <dt class="sc-label">Email</dt><dd class="sc-value"><a class="sc-value" href="mailto:ada@example.com">ada@example.com</a></dd>
|
|
326
|
-
// <dt class="sc-label">Role</dt><dd class="sc-value"><span class="sc-value">admin</span></dd>
|
|
327
|
-
// </dl>
|
|
328
|
-
|
|
329
|
-
// Editable form
|
|
330
|
-
const formHtml = renderToHtml(userSchema, {
|
|
331
|
-
value: { name: "Ada Lovelace", email: "ada@example.com", role: "admin" },
|
|
332
|
-
});
|
|
333
|
-
// → <fieldset class="sc-object">
|
|
334
|
-
// <div class="sc-field">
|
|
335
|
-
// <label class="sc-label" for="sc-name">Name</label>
|
|
336
|
-
// <input class="sc-input" type="text" name="" value="Ada Lovelace">
|
|
337
|
-
// </div>
|
|
338
|
-
// ...
|
|
339
|
-
// </fieldset>
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
All HTML output uses `sc-` prefixed classes for styling hooks. HTML is properly escaped by the serialiser — no manual escaping needed.
|
|
343
|
-
|
|
344
|
-
A default stylesheet is included:
|
|
345
|
-
|
|
346
|
-
```html
|
|
347
|
-
<link rel="stylesheet" href="node_modules/schema-components/dist/html/styles.css">
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
Or import in JS:
|
|
351
|
-
|
|
352
|
-
```ts
|
|
353
|
-
import "schema-components/styles.css";
|
|
354
|
-
```
|
|
355
|
-
|
|
356
|
-
### Structured HTML construction
|
|
357
|
-
|
|
358
|
-
The HTML renderer uses a typed `h()` builder instead of string templates. This gives compile-time safety and automatic escaping:
|
|
359
|
-
|
|
360
|
-
```ts
|
|
361
|
-
import { h, serialize, raw } from "schema-components/html/html";
|
|
362
|
-
|
|
363
|
-
// Build elements — attrs are type-checked, values auto-escaped
|
|
364
|
-
const input = h("input", { type: "text", id: "name", value: userValue });
|
|
365
|
-
serialize(input); // → <input type="text" id="name" value="Ada">
|
|
366
|
-
|
|
367
|
-
// Embed pre-serialised HTML (from child renderers)
|
|
368
|
-
const div = h("div", { class: "field" }, raw(childHtml));
|
|
369
|
-
serialize(div);
|
|
370
|
-
```
|
|
371
|
-
|
|
372
|
-
The builder handles void elements (`<input>`, `<br>`, etc.), boolean attributes (`checked`, `disabled`), fragments, and nested children.
|
|
373
|
-
|
|
374
|
-
### Streaming HTML
|
|
375
|
-
|
|
376
|
-
Three output formats for incremental rendering:
|
|
377
|
-
|
|
378
|
-
```ts
|
|
379
|
-
import { renderToHtmlChunks } from "schema-components/html/renderToHtmlStream";
|
|
380
|
-
import { renderToHtmlStream } from "schema-components/html/renderToHtmlStream";
|
|
381
|
-
import { renderToHtmlReadable } from "schema-components/html/renderToHtmlStream";
|
|
382
|
-
|
|
383
|
-
// Sync iterable — chunks yielded at field/item/entry boundaries
|
|
384
|
-
const chunks: string[] = [...renderToHtmlChunks(schema, { value })];
|
|
385
|
-
|
|
386
|
-
// Async iterable — yields control to event loop between chunks
|
|
387
|
-
for await (const chunk of renderToHtmlStream(schema, { value })) {
|
|
388
|
-
res.write(chunk);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
// Web ReadableStream — for Response, TransformStream, etc.
|
|
392
|
-
return new Response(renderToHtmlReadable(schema, { value }), {
|
|
393
|
-
headers: { "Content-Type": "text/html" },
|
|
394
|
-
});
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Concatenating all chunks produces identical output to `renderToHtml`.
|
|
398
|
-
|
|
399
|
-
### Accessibility
|
|
400
|
-
|
|
401
|
-
The HTML renderer produces WAI-ARIA-compliant markup:
|
|
402
|
-
|
|
403
|
-
| Attribute | When |
|
|
404
|
-
|---|---|
|
|
405
|
-
| `id="<key>"` | All editable inputs |
|
|
406
|
-
| `aria-required="true"` | Required fields (`isOptional === false`) |
|
|
407
|
-
| `aria-describedby="<id>-hint"` | Fields with constraints (min/max/length/pattern) |
|
|
408
|
-
| `aria-readonly="true"` | Read-only presentation spans |
|
|
409
|
-
| `aria-label="<description>"` | Checkboxes (no visible text node) |
|
|
410
|
-
| `role="group"` | Record containers |
|
|
411
|
-
| `aria-label` on `<fieldset>` | Object with description |
|
|
412
|
-
| `<small class="sc-hint">` | Constraint hint text |
|
|
413
|
-
| `<span class="sc-required" aria-hidden="true">*` | Required field indicator |
|
|
414
|
-
|
|
415
|
-
### Custom HTML resolver
|
|
416
|
-
|
|
417
|
-
```ts
|
|
418
|
-
import { renderToHtml } from "schema-components/html/renderToHtml";
|
|
419
|
-
import type { HtmlResolver, HtmlRenderProps } from "schema-components/html/renderToHtml";
|
|
420
|
-
|
|
421
|
-
const tailwindResolver: HtmlResolver = {
|
|
422
|
-
string: (props: HtmlRenderProps) => {
|
|
423
|
-
if (props.readOnly) {
|
|
424
|
-
return `<span class="text-sm text-gray-700">${typeof props.value === "string" ? props.value : ""}</span>`;
|
|
425
|
-
}
|
|
426
|
-
return `<input class="border rounded px-2 py-1" type="text" value="${typeof props.value === "string" ? props.value : "">">`;
|
|
427
|
-
},
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const html = renderToHtml(schema, { value, readOnly: true, resolver: tailwindResolver });
|
|
431
|
-
```
|
|
432
|
-
|
|
433
|
-
Custom resolvers fall back to the default for any type you don't override.
|
|
434
|
-
|
|
435
|
-
## Custom widgets
|
|
436
|
-
|
|
437
|
-
Widgets let you override rendering for specific fields using `.meta({ component: name })`. Three scopes are available, checked in order:
|
|
438
|
-
|
|
439
|
-
1. **Instance** — `widgets` prop on `<SchemaComponent>`
|
|
440
|
-
2. **Context** — `widgets` prop on `<SchemaProvider>`
|
|
441
|
-
3. **Global** — `registerWidget()` for app-wide defaults
|
|
442
|
-
|
|
443
|
-
If none match, the theme adapter or headless default handles the field.
|
|
444
|
-
|
|
445
|
-
### Global registration
|
|
446
|
-
|
|
447
|
-
```tsx
|
|
448
|
-
import { registerWidget } from "schema-components/react/SchemaComponent";
|
|
449
|
-
|
|
450
|
-
registerWidget("richtext", ({ value, onChange }) => (
|
|
451
|
-
<RichTextEditor value={value} onChange={onChange} />
|
|
452
|
-
));
|
|
453
|
-
|
|
454
|
-
const schema = z.object({
|
|
455
|
-
bio: z.string().meta({ component: "richtext" }),
|
|
456
|
-
});
|
|
457
|
-
```
|
|
458
|
-
|
|
459
|
-
### Context-scoped widgets
|
|
460
|
-
|
|
461
|
-
Share widgets across a subtree via `<SchemaProvider>`:
|
|
462
|
-
|
|
463
|
-
```tsx
|
|
464
|
-
import { SchemaProvider } from "schema-components/react/SchemaComponent";
|
|
465
|
-
import type { WidgetMap } from "schema-components/react/SchemaComponent";
|
|
466
|
-
|
|
467
|
-
const adminWidgets: WidgetMap = new Map([
|
|
468
|
-
["richtext", ({ value, onChange }) => <RichTextEditor value={value} onChange={onChange} />],
|
|
469
|
-
["avatar", ({ value, onChange }) => <AvatarUploader value={value} onChange={onChange} />],
|
|
470
|
-
]);
|
|
471
|
-
|
|
472
|
-
<SchemaProvider resolver={shadcnResolver} widgets={adminWidgets}>
|
|
473
|
-
<SchemaComponent schema={userSchema} value={user} onChange={setUser} />
|
|
474
|
-
<SchemaComponent schema={profileSchema} value={profile} onChange={setProfile} />
|
|
475
|
-
</SchemaProvider>
|
|
476
|
-
```
|
|
477
|
-
|
|
478
|
-
### Instance-scoped widgets
|
|
479
|
-
|
|
480
|
-
Override widgets for a single form:
|
|
481
|
-
|
|
482
|
-
```tsx
|
|
483
|
-
const formWidgets: WidgetMap = new Map([
|
|
484
|
-
["richtext", ({ value, onChange }) => <SimpleTextarea value={value} onChange={onChange} />],
|
|
485
|
-
]);
|
|
486
|
-
|
|
487
|
-
<SchemaComponent schema={formSchema} value={form} widgets={formWidgets} />
|
|
488
|
-
```
|
|
489
|
-
|
|
490
|
-
### Resolution order
|
|
491
|
-
|
|
492
|
-
```.meta({ component }) hint → instance widgets → context widgets → global registerWidget() → theme adapter → headless default
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
Instance overrides context. Context overrides global. Unhinted fields skip the widget layer entirely.
|
|
496
|
-
|
|
497
|
-
### `WidgetMap` type
|
|
498
|
-
|
|
499
|
-
```tsx
|
|
500
|
-
import type { WidgetMap } from "schema-components/react/SchemaComponent";
|
|
501
|
-
|
|
502
|
-
// ReadonlyMap<string, (props: RenderProps) => unknown>
|
|
503
|
-
const widgets: WidgetMap = new Map([
|
|
504
|
-
["name", (props) => <MyInput {...props} />],
|
|
505
|
-
]);
|
|
506
|
-
```
|
|
507
|
-
|
|
508
|
-
Server Components: `<SchemaView>` accepts a `widgets` prop directly (no React context available):
|
|
509
|
-
|
|
510
|
-
```tsx
|
|
511
|
-
<SchemaView schema={schema} value={data} widgets={serverWidgets} />
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
## Validation
|
|
515
|
-
|
|
516
|
-
```tsx
|
|
517
|
-
<SchemaComponent
|
|
518
|
-
schema={userSchema}
|
|
519
|
-
value={user}
|
|
520
|
-
onChange={setUser}
|
|
521
|
-
validate
|
|
522
|
-
onValidationError={(error) => console.error(error)}
|
|
523
|
-
/>
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
Validation uses the original Zod schema (if input was Zod) or `z.fromJSONSchema()` (if input was JSON Schema / OpenAPI).
|
|
527
|
-
|
|
528
|
-
### Per-field validation errors
|
|
529
|
-
|
|
530
|
-
Add `onValidationError` to individual field overrides to receive errors for specific fields:
|
|
531
|
-
|
|
532
|
-
```tsx
|
|
533
|
-
<SchemaComponent
|
|
534
|
-
schema={userSchema}
|
|
535
|
-
value={user}
|
|
536
|
-
onChange={setUser}
|
|
537
|
-
validate
|
|
538
|
-
fields={{
|
|
539
|
-
email: { onValidationError: (err) => setEmailError(err) },
|
|
540
|
-
name: { onValidationError: (err) => setNameError(err) },
|
|
541
|
-
}}
|
|
542
|
-
/>
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
Errors are dispatched based on Zod error paths. The root-level `onValidationError` still receives all errors.
|
|
546
|
-
|
|
547
|
-
## Discriminated unions
|
|
548
|
-
|
|
549
|
-
Discriminated unions (`z.discriminatedUnion` or JSON Schema `oneOf` with `const` properties) render as tabbed panels. Each tab is labelled by the discriminator's `const` value. Clicking a tab resets the value with the new discriminator.
|
|
550
|
-
|
|
551
|
-
```tsx
|
|
552
|
-
const payment = z.discriminatedUnion("method", [
|
|
553
|
-
z.object({
|
|
554
|
-
method: z.literal("card"),
|
|
555
|
-
cardNumber: z.string(),
|
|
556
|
-
expiry: z.string(),
|
|
557
|
-
}),
|
|
558
|
-
z.object({
|
|
559
|
-
method: z.literal("bank"),
|
|
560
|
-
accountNumber: z.string(),
|
|
561
|
-
sortCode: z.string(),
|
|
562
|
-
}),
|
|
563
|
-
]);
|
|
564
|
-
|
|
565
|
-
<SchemaComponent schema={payment} value={{ method: "card", cardNumber: "4111...", expiry: "12/28" }} />
|
|
566
|
-
```
|
|
567
|
-
|
|
568
|
-
In read-only mode, only the active variant is rendered (no tabs).
|
|
569
|
-
|
|
570
|
-
## Date and time inputs
|
|
571
|
-
|
|
572
|
-
String schemas with `format: "date"`, `format: "time"`, or `format: "date-time"` render as the corresponding HTML5 input types:
|
|
573
|
-
|
|
574
|
-
```tsx
|
|
575
|
-
const eventSchema = z.object({
|
|
576
|
-
date: z.string().meta({ format: "date" }),
|
|
577
|
-
startTime: z.string().meta({ format: "time" }),
|
|
578
|
-
createdAt: z.string().meta({ format: "date-time" }),
|
|
579
|
-
});
|
|
580
|
-
```
|
|
581
|
-
|
|
582
|
-
This produces `<input type="date">`, `<input type="time">`, and `<input type="datetime-local">` respectively. In read-only mode, dates are formatted using `toLocaleDateString()` / `toLocaleString()`.
|
|
583
|
-
|
|
584
|
-
## Schema defaults
|
|
585
|
-
|
|
586
|
-
Default values from `z.string().default("hello")` or JSON Schema `"default": "hello"` are used when the `value` prop is `undefined`:
|
|
587
|
-
|
|
588
|
-
```tsx
|
|
589
|
-
const schema = z.object({
|
|
590
|
-
name: z.string().default("World"),
|
|
591
|
-
count: z.number().default(0),
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
// Renders with "World" and 0 pre-filled
|
|
595
|
-
<SchemaComponent schema={schema} />
|
|
596
|
-
```
|
|
597
|
-
|
|
598
|
-
Defaults propagate through nested objects — each field uses its own default independently.
|
|
599
|
-
|
|
600
|
-
## Field visibility
|
|
601
|
-
|
|
602
|
-
Hide fields conditionally using the `visible` override:
|
|
603
|
-
|
|
604
|
-
```tsx
|
|
605
|
-
<SchemaComponent
|
|
606
|
-
schema={paymentSchema}
|
|
607
|
-
value={payment}
|
|
608
|
-
fields={{
|
|
609
|
-
cardNumber: { visible: payment.method === "card" },
|
|
610
|
-
sortCode: { visible: payment.method === "bank" },
|
|
611
|
-
}}
|
|
612
|
-
/>
|
|
613
|
-
```
|
|
614
|
-
|
|
615
|
-
When `visible: false`, the field is completely removed — no label, no empty placeholder, no hidden input. The parent component controls visibility based on the current value.
|
|
616
|
-
|
|
617
|
-
## Field ordering
|
|
618
|
-
|
|
619
|
-
Control the order fields appear in rendered objects using `order`:
|
|
620
|
-
|
|
621
|
-
```tsx
|
|
622
|
-
<SchemaComponent
|
|
623
|
-
schema={userSchema}
|
|
624
|
-
value={user}
|
|
625
|
-
fields={{
|
|
626
|
-
email: { order: 1 },
|
|
627
|
-
name: { order: 2 },
|
|
628
|
-
role: { order: 3 },
|
|
629
|
-
}}
|
|
630
|
-
/>
|
|
631
|
-
```
|
|
632
|
-
|
|
633
|
-
Lower `order` values render first. Fields without `order` keep their insertion order and appear after ordered fields. Can also be set in schema metadata:
|
|
634
|
-
|
|
635
|
-
```tsx
|
|
636
|
-
const schema = z.object({
|
|
637
|
-
summary: z.string().meta({ order: 1 }),
|
|
638
|
-
title: z.string().meta({ order: 2 }),
|
|
639
|
-
});
|
|
640
|
-
```
|
|
641
|
-
|
|
642
|
-
## Server Components
|
|
643
|
-
|
|
644
|
-
For read-only rendering in a React Server Component, use `<SchemaView>`. It has zero hooks — no `useContext`, no `useMemo`, no `useCallback` — so it works without the `"use client"` directive.
|
|
645
|
-
|
|
646
|
-
```tsx
|
|
647
|
-
import { SchemaView } from "schema-components/react/SchemaView";
|
|
648
|
-
|
|
649
|
-
export default async function Page() {
|
|
650
|
-
const user = await getUser();
|
|
651
|
-
return <SchemaView schema={userSchema} value={user} />;
|
|
652
|
-
}
|
|
653
|
-
```
|
|
654
|
-
|
|
655
|
-
`SchemaView` always renders read-only. For editable forms, use `<SchemaComponent>` (which requires `"use client"`).
|
|
656
|
-
|
|
657
|
-
Pass the resolver explicitly since React context is unavailable in Server Components:
|
|
658
|
-
|
|
659
|
-
```tsx
|
|
660
|
-
import { SchemaView } from "schema-components/react/SchemaView";
|
|
661
|
-
import { shadcnResolver } from "schema-components/themes/shadcn";
|
|
662
|
-
|
|
663
|
-
<SchemaView schema={schema} value={data} resolver={shadcnResolver} />
|
|
664
|
-
```
|
|
665
|
-
|
|
666
|
-
`SchemaView` produces identical output to `<SchemaComponent readOnly>` — verified by parity tests.
|
|
667
|
-
|
|
668
|
-
## File uploads
|
|
669
|
-
|
|
670
|
-
String schemas with `format: "binary"` render as `<input type="file">`. Use `contentMediaType` to restrict accepted MIME types:
|
|
671
|
-
|
|
672
|
-
```tsx
|
|
673
|
-
const schema = z.object({
|
|
674
|
-
avatar: z.string().meta({ format: "binary" }),
|
|
675
|
-
resume: z.string().meta({ format: "binary", contentMediaType: "application/pdf" }),
|
|
676
|
-
});
|
|
677
|
-
```
|
|
678
|
-
|
|
679
|
-
In read-only mode, file fields display a static label ("File field") since there is no value to show. The `onChange` callback receives the `File` object from the browser.
|
|
680
|
-
|
|
681
|
-
## Error handling
|
|
682
|
-
|
|
683
|
-
Typed errors with `onError` callback for graceful degradation:
|
|
684
|
-
|
|
685
|
-
```tsx
|
|
686
|
-
import { SchemaErrorBoundary } from "schema-components/react/SchemaErrorBoundary";
|
|
687
|
-
import { SchemaComponent } from "schema-components/react/SchemaComponent";
|
|
688
|
-
|
|
689
|
-
// Error boundary catches render errors from theme adapters
|
|
690
|
-
<SchemaErrorBoundary fallback={(error, reset) => <p>Error: {error.message}</p>}>
|
|
691
|
-
<SchemaComponent schema={schema} value={data} />
|
|
692
|
-
</SchemaErrorBoundary>
|
|
693
|
-
|
|
694
|
-
// Per-component error callback
|
|
695
|
-
<SchemaComponent
|
|
696
|
-
schema={schema}
|
|
697
|
-
value={data}
|
|
698
|
-
onError={(error) => {
|
|
699
|
-
console.error(error);
|
|
700
|
-
return null; // graceful degradation
|
|
701
|
-
}}
|
|
702
|
-
/>
|
|
703
|
-
```
|
|
704
|
-
|
|
705
|
-
Without `onError`, errors re-throw. Error hierarchy: `SchemaError` → `SchemaNormalisationError` | `SchemaRenderError` | `SchemaFieldError`.
|
|
706
|
-
|
|
707
|
-
## Architecture
|
|
708
|
-
|
|
709
|
-
```
|
|
710
|
-
schema-components
|
|
711
|
-
├── core # JSON Schema walker, ComponentResolver, RenderProps, typed errors, type guards
|
|
712
|
-
├── react # SchemaComponent ("use client"), SchemaView (server component), headless renderer, error boundary
|
|
713
|
-
├── openapi # Document parser, ApiOperation, ApiParameters, ApiRequestBody, ApiResponse
|
|
714
|
-
├── html # h() builder, renderToHtml, streaming renderers, ARIA helpers
|
|
715
|
-
└── themes # shadcn, MUI, custom adapters (separate packages)
|
|
716
|
-
```
|
|
717
|
-
|
|
718
|
-
Every module is imported directly — no barrel files. Organised exports:
|
|
719
|
-
|
|
720
|
-
```
|
|
721
|
-
schema-components/core/* # Walker, types, guards, errors, resolver
|
|
722
|
-
schema-components/react/* # SchemaComponent, SchemaView, SchemaErrorBoundary, headless
|
|
723
|
-
schema-components/openapi/* # Parser, ApiOperation, ApiParameters, etc.
|
|
724
|
-
schema-components/html/* # renderToHtml, renderToHtmlChunks, h() builder, styles
|
|
725
|
-
schema-components/themes/* # shadcn, MUI, custom adapters
|
|
726
|
-
schema-components/styles.css # Default stylesheet for HTML output
|
|
727
|
-
```
|