schema-components 2.1.0 → 3.0.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.
Files changed (44) hide show
  1. package/README.md +41 -6
  2. package/dist/{SchemaComponent-B__6-5-E.d.mts → SchemaComponent-CRgCVDhz.d.mts} +27 -15
  3. package/dist/{SchemaComponent-BxzzsHsK.mjs → SchemaComponent-Cga5oJfP.mjs} +3 -3
  4. package/dist/core/renderField.mjs +41 -7
  5. package/dist/html/streamRenderers.d.mts +12 -3
  6. package/dist/html/streamRenderers.mjs +56 -10
  7. package/dist/lit/SchemaComponent.d.mts +2 -2
  8. package/dist/lit/SchemaComponent.mjs +1 -1
  9. package/dist/lit/SchemaField.d.mts +1 -1
  10. package/dist/lit/SchemaField.mjs +1 -1
  11. package/dist/lit/SchemaView.mjs +1 -1
  12. package/dist/lit/defaultResolver.mjs +1 -1
  13. package/dist/lit/registry.mjs +1 -1
  14. package/dist/preact/SchemaComponent.d.mts +1 -1
  15. package/dist/react/SchemaComponent.d.mts +1 -1
  16. package/dist/react/SchemaComponent.mjs +6 -6
  17. package/dist/react/SchemaView.d.mts +16 -11
  18. package/dist/react/SchemaView.mjs +2 -2
  19. package/dist/solid/SchemaComponent.d.mts +6 -6
  20. package/dist/solid/SchemaComponent.mjs +1 -1
  21. package/dist/solid/SchemaField.d.mts +3 -3
  22. package/dist/solid/SchemaField.mjs +1 -1
  23. package/dist/solid/SchemaView.d.mts +5 -5
  24. package/dist/solid/SchemaView.mjs +1 -1
  25. package/package.json +5 -3
  26. package/src/svelte/SchemaComponent.svelte +3 -3
  27. package/src/svelte/SchemaField.svelte +3 -3
  28. package/src/svelte/SchemaView.svelte +3 -3
  29. package/src/vue/SchemaComponent.vue +274 -0
  30. package/src/vue/SchemaErrorBoundary.vue +60 -0
  31. package/src/vue/SchemaField.vue +178 -0
  32. package/src/vue/SchemaProvider.vue +39 -0
  33. package/src/vue/SchemaView.vue +198 -0
  34. package/src/vue/VNodeHost.ts +32 -0
  35. package/src/vue/contexts.ts +116 -0
  36. package/src/vue/eventTargets.ts +35 -0
  37. package/src/vue/headless.ts +61 -0
  38. package/src/vue/idPrefix.ts +79 -0
  39. package/src/vue/renderField.ts +182 -0
  40. package/src/vue/renderers.ts +1297 -0
  41. package/src/vue/resolver.ts +45 -0
  42. package/src/vue/types.ts +140 -0
  43. package/src/vue/vue-shim.d.ts +25 -0
  44. package/src/vue/widget.ts +51 -0
@@ -42,17 +42,17 @@ declare function SchemaProvider(props: {
42
42
  *
43
43
  * @group Components
44
44
  */
45
- interface SchemaComponentProps<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
45
+ interface SchemaComponentProps<T = unknown, SchemaRef extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
46
46
  /** Zod schema, JSON Schema object, or OpenAPI document. */
47
47
  schema: RejectUnrepresentableZod<T>;
48
48
  /** For OpenAPI: a ref string like `"#/components/schemas/User"`. */
49
- ref?: Ref;
49
+ schemaRef?: SchemaRef;
50
50
  /** Which side of every transform / pipe / codec to render. */
51
51
  io?: Mode;
52
52
  /** Current value to render — typed against the schema's inferred shape. */
53
- value?: InferSchemaValue<T, Ref, Mode>;
53
+ value?: InferSchemaValue<T, SchemaRef, Mode>;
54
54
  /** Called when the value changes; receives the next value. */
55
- onChange?: (value: InferSchemaValue<T, Ref, Mode>) => void;
55
+ onChange?: (value: InferSchemaValue<T, SchemaRef, Mode>) => void;
56
56
  /** Run `safeParse` / `safeEncode` on change and route errors. */
57
57
  validate?: boolean;
58
58
  /** Called with the validation error when validation fails. */
@@ -64,7 +64,7 @@ interface SchemaComponentProps<T = unknown, Ref extends string | undefined = und
64
64
  /** When true, any diagnostic becomes a thrown error. */
65
65
  strict?: boolean;
66
66
  /** Per-field meta overrides — nested object mirroring schema shape. */
67
- fields?: InferFields<T, Ref>;
67
+ fields?: InferFields<T, SchemaRef>;
68
68
  /** Meta overrides applied to the root schema. */
69
69
  meta?: SchemaMeta;
70
70
  /** Convenience: sets readOnly on all fields. */
@@ -131,6 +131,6 @@ declare function renderField(tree: WalkedField, value: unknown, onChange: (v: un
131
131
  * <SchemaComponent schema={userSchema} value={user} onChange={setUser} />
132
132
  * ```
133
133
  */
134
- declare function SchemaComponent<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output">(props: SchemaComponentProps<T, Ref, Mode>): JSX.Element;
134
+ declare function SchemaComponent<T = unknown, SchemaRef extends string | undefined = undefined, Mode extends SchemaIoSide = "output">(props: SchemaComponentProps<T, SchemaRef, Mode>): JSX.Element;
135
135
  //#endregion
136
136
  export { type InferFields, type InferredInputValue, type InferredOutputValue, type InferredValue, SchemaComponent, SchemaComponentProps, SchemaProvider, joinPath, renderField, sanitisePrefix };
@@ -232,7 +232,7 @@ function SchemaComponent(props) {
232
232
  const onDiagnostic = rest.onDiagnostic;
233
233
  const strict = rest.strict;
234
234
  const io = rest.io;
235
- const refInput = rest.ref;
235
+ const refInput = rest.schemaRef;
236
236
  const onError = rest.onError;
237
237
  const idPrefix = rest.idPrefix;
238
238
  const mergedMeta = { ...componentMeta };
@@ -16,10 +16,10 @@ type InferSchemaType<T> = T extends z.ZodType ? z.infer<T> : T extends object ?
16
16
  *
17
17
  * @group Components
18
18
  */
19
- interface SchemaFieldProps<T = unknown, Ref extends string | undefined = undefined, P extends string = PathOfType<InferSchemaType<T>> | (string extends PathOfType<InferSchemaType<T>> ? string : never)> {
19
+ interface SchemaFieldProps<T = unknown, SchemaRef extends string | undefined = undefined, P extends string = PathOfType<InferSchemaType<T>> | (string extends PathOfType<InferSchemaType<T>> ? string : never)> {
20
20
  path: P;
21
21
  schema: RejectUnrepresentableZod<T>;
22
- ref?: Ref;
22
+ schemaRef?: SchemaRef;
23
23
  value?: unknown;
24
24
  onChange?: (value: unknown) => void;
25
25
  meta?: SchemaMeta;
@@ -35,6 +35,6 @@ interface SchemaFieldProps<T = unknown, Ref extends string | undefined = undefin
35
35
  *
36
36
  * @group Components
37
37
  */
38
- declare function SchemaField<T = unknown, Ref extends string | undefined = undefined, P extends string = PathOfType<InferSchemaType<T>> | (string extends PathOfType<InferSchemaType<T>> ? string : never)>(props: SchemaFieldProps<T, Ref, P>): JSX.Element;
38
+ declare function SchemaField<T = unknown, SchemaRef extends string | undefined = undefined, P extends string = PathOfType<InferSchemaType<T>> | (string extends PathOfType<InferSchemaType<T>> ? string : never)>(props: SchemaFieldProps<T, SchemaRef, P>): JSX.Element;
39
39
  //#endregion
40
40
  export { SchemaField, SchemaFieldProps };
@@ -40,7 +40,7 @@ function SchemaField(props) {
40
40
  let rootMeta;
41
41
  let rootDocument;
42
42
  try {
43
- const normalised = normaliseSchema(props.schema, props.ref);
43
+ const normalised = normaliseSchema(props.schema, props.schemaRef);
44
44
  jsonSchema = normalised.jsonSchema;
45
45
  zodSchema = normalised.zodSchema;
46
46
  rootMeta = normalised.rootMeta;
@@ -17,12 +17,12 @@ import { JSX } from "solid-js";
17
17
  *
18
18
  * @group Components
19
19
  */
20
- interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
20
+ interface SchemaViewProps<T = unknown, SchemaRef extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
21
21
  schema: RejectUnrepresentableZod<T>;
22
- ref?: Ref;
22
+ schemaRef?: SchemaRef;
23
23
  io?: Mode;
24
- value?: InferredValue<T, Ref, undefined, Mode>;
25
- fields?: InferFields<T, Ref>;
24
+ value?: InferredValue<T, SchemaRef, undefined, Mode>;
25
+ fields?: InferFields<T, SchemaRef>;
26
26
  meta?: SchemaMeta;
27
27
  description?: string;
28
28
  /** Theme resolver. Falls back to the headless resolver if omitted. */
@@ -49,6 +49,6 @@ interface SchemaViewProps<T = unknown, Ref extends string | undefined = undefine
49
49
  * }
50
50
  * ```
51
51
  */
52
- declare function SchemaView<T = unknown, Ref extends string | undefined = undefined, Mode extends SchemaIoSide = "output">(props: SchemaViewProps<T, Ref, Mode>): JSX.Element;
52
+ declare function SchemaView<T = unknown, SchemaRef extends string | undefined = undefined, Mode extends SchemaIoSide = "output">(props: SchemaViewProps<T, SchemaRef, Mode>): JSX.Element;
53
53
  //#endregion
54
54
  export { SchemaView, SchemaViewProps };
@@ -66,7 +66,7 @@ function SchemaView(props) {
66
66
  ...diagnostics !== void 0 ? { diagnostics } : {},
67
67
  ...props.io !== void 0 ? { io: props.io } : {}
68
68
  } : void 0;
69
- const normalised = normaliseSchema(props.schema, props.ref, normaliseOptions);
69
+ const normalised = normaliseSchema(props.schema, props.schemaRef, normaliseOptions);
70
70
  jsonSchema = normalised.jsonSchema;
71
71
  rootMeta = normalised.rootMeta;
72
72
  rootDocument = normalised.rootDocument;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "schema-components",
3
- "version": "2.1.0",
3
+ "version": "3.0.0",
4
4
  "description": "React components that render UI from Zod schemas, JSON Schema, and OpenAPI documents",
5
5
  "type": "module",
6
6
  "exports": {
@@ -29,9 +29,10 @@
29
29
  "types": "./dist/themes/*.d.mts",
30
30
  "import": "./dist/themes/*.mjs"
31
31
  },
32
+ "./vue/*.vue": "./src/vue/*.vue",
32
33
  "./vue/*": {
33
- "types": "./dist/vue/*.d.mts",
34
- "import": "./dist/vue/*.mjs"
34
+ "types": "./src/vue/*.ts",
35
+ "import": "./src/vue/*.ts"
35
36
  },
36
37
  "./svelte/*.svelte": "./src/svelte/*.svelte",
37
38
  "./svelte/renderers/*.svelte": "./src/svelte/renderers/*.svelte",
@@ -52,6 +53,7 @@
52
53
  "files": [
53
54
  "dist",
54
55
  "src/svelte",
56
+ "src/vue",
55
57
  "LICENSE",
56
58
  "README.md",
57
59
  "CHANGELOG.md"
@@ -78,7 +78,7 @@
78
78
  /** Zod 4, JSON Schema, or OpenAPI document. */
79
79
  schema: T;
80
80
  /** OpenAPI ref string, e.g. "#/components/schemas/User". */
81
- ref?: Ref;
81
+ schemaRef?: Ref;
82
82
  /** Direction (`"output"` / `"input"`) for codec / transform schemas. */
83
83
  io?: SchemaIoSide;
84
84
  /** Current value to render. */
@@ -119,7 +119,7 @@
119
119
 
120
120
  const {
121
121
  schema,
122
- ref,
122
+ schemaRef,
123
123
  io,
124
124
  value,
125
125
  onChange,
@@ -176,7 +176,7 @@
176
176
  }
177
177
 
178
178
  const normalisedResult = $derived<NormalisedShape | SchemaError>(
179
- normaliseSafely(schema, ref, io, diagnostics)
179
+ normaliseSafely(schema, schemaRef, io, diagnostics)
180
180
  );
181
181
 
182
182
  /**
@@ -40,7 +40,7 @@
40
40
  /** The schema to extract the field from. */
41
41
  schema: T;
42
42
  /** OpenAPI ref string. */
43
- ref?: Ref;
43
+ schemaRef?: Ref;
44
44
  /** Direction (`"output"` / `"input"`) for codec / transform schemas. */
45
45
  io?: SchemaIoSide;
46
46
  /** Current value of the root schema. */
@@ -59,7 +59,7 @@
59
59
  const {
60
60
  path,
61
61
  schema,
62
- ref,
62
+ schemaRef,
63
63
  io,
64
64
  value,
65
65
  onChange,
@@ -80,7 +80,7 @@
80
80
  }
81
81
 
82
82
  const normalisedResult = $derived<NormalisedShape>(
83
- normaliseOrThrow(schema, ref, io)
83
+ normaliseOrThrow(schema, schemaRef, io)
84
84
  );
85
85
 
86
86
  const walkOptions = $derived<WalkOptions>({
@@ -51,7 +51,7 @@
51
51
 
52
52
  interface Props {
53
53
  schema: T;
54
- ref?: Ref;
54
+ schemaRef?: Ref;
55
55
  io?: SchemaIoSide;
56
56
  value?: InferredValue<T, Ref, undefined, "output">;
57
57
  fields?: InferFields<T, Ref>;
@@ -68,7 +68,7 @@
68
68
 
69
69
  const {
70
70
  schema,
71
- ref,
71
+ schemaRef,
72
72
  io,
73
73
  value,
74
74
  fields,
@@ -113,7 +113,7 @@
113
113
  }
114
114
 
115
115
  const normalisedResult = $derived<NormalisedShape>(
116
- normaliseOrThrow(schema, ref, io, diagnostics)
116
+ normaliseOrThrow(schema, schemaRef, io, diagnostics)
117
117
  );
118
118
 
119
119
  const fieldsRecord = $derived(toRecordOrUndefined(fields));
@@ -0,0 +1,274 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<SchemaComponent>` — Vue counterpart of the React `<SchemaComponent>`.
4
+ *
5
+ * Auto-detects the input format, normalises to JSON Schema via the
6
+ * adapter, walks the JSON Schema tree, and delegates per-field
7
+ * rendering to the {@link VueComponentResolver} supplied via
8
+ * `<SchemaProvider>` — falling back to the headless renderer when no
9
+ * provider is present.
10
+ *
11
+ * `onChange` semantics: the component accepts BOTH a `v-model`
12
+ * binding (Vue-idiomatic — `modelValue` prop +
13
+ * `update:modelValue` emit) and an explicit `onChange` callback prop
14
+ * (matching the React adapter). It also emits a `change` event for
15
+ * Vue authors who prefer event listeners. All three surfaces fire
16
+ * together so consumers may use whichever idiom suits them.
17
+ *
18
+ * @group Components
19
+ */
20
+ import { computed, h, toRaw, type VNode } from "vue";
21
+ import { walk } from "../core/walker.ts";
22
+ import type { WalkOptions } from "../core/walkBuilders.ts";
23
+ import { normaliseSchema } from "../core/adapter.ts";
24
+ import type { SchemaMeta, WalkedField } from "../core/types.ts";
25
+ import type { Diagnostic, DiagnosticsOptions } from "../core/diagnostics.ts";
26
+ import { SchemaNormalisationError } from "../core/errors.ts";
27
+ import { toRecordOrUndefined } from "../core/guards.ts";
28
+ import { VueResolverContext, VueWidgetsContext } from "./contexts.ts";
29
+ import { vueRenderField } from "./renderField.ts";
30
+ import { deriveIdPrefix, joinPath } from "./idPrefix.ts";
31
+ import type {
32
+ VueComponentResolver,
33
+ VueRenderProps,
34
+ VueWidgetMap,
35
+ } from "./types.ts";
36
+ import { VNodeHost } from "./VNodeHost.ts";
37
+
38
+ const props = withDefaults(
39
+ defineProps<{
40
+ /** Zod schema, JSON Schema object, or OpenAPI document. */
41
+ schema: unknown;
42
+ /** For OpenAPI: a ref string like `#/components/schemas/User`. */
43
+ schemaRef?: string;
44
+ /** v-model binding for the current value. */
45
+ modelValue?: unknown;
46
+ /**
47
+ * Explicit `onChange` callback — wired in parallel with
48
+ * `update:modelValue` so consumers may use either surface.
49
+ */
50
+ onChange?: (value: unknown) => void;
51
+ /** Convenience: sets `readOnly` on all fields. */
52
+ readOnly?: boolean;
53
+ /** Convenience: sets `writeOnly` on all fields. */
54
+ writeOnly?: boolean;
55
+ /** Convenience: sets `description` on the root. */
56
+ description?: string;
57
+ /** Meta overrides applied to the root schema. */
58
+ meta?: SchemaMeta;
59
+ /** Per-field meta overrides — nested object mirroring schema shape. */
60
+ fields?: Record<string, unknown>;
61
+ /** Theme resolver. Overrides the context resolver when supplied. */
62
+ resolver?: VueComponentResolver;
63
+ /** Instance-scoped widgets — override context and global widgets. */
64
+ widgets?: VueWidgetMap;
65
+ /** Deterministic id prefix. Defaults to a per-instance `useId()` value. */
66
+ idPrefix?: string;
67
+ /** Called with each diagnostic emitted during schema processing. */
68
+ onDiagnostic?: (diagnostic: Diagnostic) => void;
69
+ /** When `true`, any diagnostic becomes a thrown error. */
70
+ strict?: boolean;
71
+ /** Called when schema normalisation fails. */
72
+ onError?: (error: SchemaNormalisationError) => void;
73
+ }>(),
74
+ {
75
+ schemaRef: undefined,
76
+ modelValue: undefined,
77
+ onChange: undefined,
78
+ readOnly: false,
79
+ writeOnly: false,
80
+ description: undefined,
81
+ meta: () => ({}),
82
+ fields: undefined,
83
+ resolver: undefined,
84
+ widgets: undefined,
85
+ idPrefix: undefined,
86
+ onDiagnostic: undefined,
87
+ strict: false,
88
+ onError: undefined,
89
+ }
90
+ );
91
+
92
+ const emit = defineEmits<{
93
+ "update:modelValue": [value: unknown];
94
+ change: [value: unknown];
95
+ }>();
96
+
97
+ // Consume the resolver and widget contexts. Both ports return
98
+ // `undefined` when no provider is mounted in scope — the dispatcher
99
+ // then falls through to the headless resolver.
100
+ const contextResolver = VueResolverContext.consume();
101
+ const contextWidgets = VueWidgetsContext.consume();
102
+
103
+ const rootPath = computed(() => deriveIdPrefix(props.idPrefix));
104
+
105
+ const mergedMeta = computed<SchemaMeta>(() => {
106
+ const merged: SchemaMeta = { ...props.meta };
107
+ if (props.readOnly) merged.readOnly = true;
108
+ if (props.writeOnly) merged.writeOnly = true;
109
+ if (props.description !== undefined) merged.description = props.description;
110
+ return merged;
111
+ });
112
+
113
+ const diagnostics = computed<DiagnosticsOptions | undefined>(() => {
114
+ if (props.onDiagnostic === undefined && !props.strict) return undefined;
115
+ const opts: DiagnosticsOptions = {};
116
+ if (props.onDiagnostic !== undefined) opts.diagnostics = props.onDiagnostic;
117
+ if (props.strict) opts.strict = true;
118
+ return opts;
119
+ });
120
+
121
+ interface Normalised {
122
+ jsonSchema: Record<string, unknown>;
123
+ rootMeta: SchemaMeta | undefined;
124
+ rootDocument: Record<string, unknown>;
125
+ error?: SchemaNormalisationError;
126
+ }
127
+
128
+ const normalised = computed<Normalised>(() => {
129
+ try {
130
+ const opts =
131
+ diagnostics.value !== undefined
132
+ ? { diagnostics: diagnostics.value }
133
+ : undefined;
134
+ // Vue wraps every reactive prop in a Proxy. Zod 4 schemas
135
+ // carry non-configurable internal data members (`_zod`) that
136
+ // the default reactivity proxy cannot mirror — accessing them
137
+ // through the proxy throws. `toRaw` recovers the original
138
+ // object before passing it into `normaliseSchema`, which does
139
+ // not need (and should not see) Vue's reactivity layer. The
140
+ // same fix is applied to `props.modelValue` further down for
141
+ // consistency with the React adapter, which receives raw
142
+ // values directly.
143
+ const rawSchema = toRaw(props.schema);
144
+ const result = normaliseSchema(rawSchema, props.schemaRef, opts);
145
+ return {
146
+ jsonSchema: result.jsonSchema,
147
+ rootMeta: result.rootMeta,
148
+ rootDocument: result.rootDocument,
149
+ };
150
+ } catch (err) {
151
+ const error =
152
+ err instanceof SchemaNormalisationError
153
+ ? err
154
+ : new SchemaNormalisationError(
155
+ err instanceof Error
156
+ ? err.message
157
+ : "Failed to normalise schema",
158
+ toRaw(props.schema),
159
+ "unknown"
160
+ );
161
+ return {
162
+ jsonSchema: {},
163
+ rootMeta: undefined,
164
+ rootDocument: {},
165
+ error,
166
+ };
167
+ }
168
+ });
169
+
170
+ const tree = computed<WalkedField | undefined>(() => {
171
+ const n = normalised.value;
172
+ if (n.error !== undefined) return undefined;
173
+ const walkOptions: WalkOptions = {
174
+ componentMeta: mergedMeta.value,
175
+ rootDocument: n.rootDocument,
176
+ };
177
+ if (n.rootMeta !== undefined) walkOptions.rootMeta = n.rootMeta;
178
+ const fieldsRecord = toRecordOrUndefined(props.fields);
179
+ if (fieldsRecord !== undefined) walkOptions.fieldOverrides = fieldsRecord;
180
+ if (diagnostics.value !== undefined)
181
+ walkOptions.diagnostics = diagnostics.value;
182
+ return walk(n.jsonSchema, walkOptions);
183
+ });
184
+
185
+ const effectiveValue = computed<unknown>(() => {
186
+ if (props.modelValue !== undefined) return props.modelValue;
187
+ return tree.value?.defaultValue;
188
+ });
189
+
190
+ function handleChange(next: unknown): void {
191
+ emit("update:modelValue", next);
192
+ // Vue auto-wires any `onChange="…"` template attribute as a
193
+ // `change` event listener (the same `on<Event>` convention React
194
+ // uses for synthetic events). To avoid invoking the same handler
195
+ // twice — once via `emit("change", …)` and once via
196
+ // `props.onChange(…)` — we emit the event when no explicit prop
197
+ // handler was supplied, and call the prop directly otherwise.
198
+ // Both paths are observable to consumers but never overlap.
199
+ if (props.onChange !== undefined) {
200
+ props.onChange(next);
201
+ } else {
202
+ emit("change", next);
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Build the recursive `renderChild` closure. Each invocation increments
208
+ * the depth counter so the dispatcher's `MAX_RENDER_DEPTH` cap fires
209
+ * on truly recursive structures rather than on shallow trees.
210
+ */
211
+ function makeRenderChild(
212
+ currentDepth: number,
213
+ parentPath: string
214
+ ): VueRenderProps["renderChild"] {
215
+ return (
216
+ childTree: WalkedField,
217
+ childValue: unknown,
218
+ childOnChange: (v: unknown) => void,
219
+ pathSuffix?: string
220
+ ) => {
221
+ const childPath = joinPath(parentPath, pathSuffix);
222
+ return vueRenderField(
223
+ childTree,
224
+ childValue,
225
+ childOnChange,
226
+ props.resolver ?? contextResolver,
227
+ makeRenderChild(currentDepth + 1, childPath),
228
+ childPath,
229
+ props.widgets,
230
+ contextWidgets,
231
+ currentDepth + 1
232
+ );
233
+ };
234
+ }
235
+
236
+ /**
237
+ * Reactive root VNode. Recomputed whenever any reactive dependency
238
+ * (props, contexts, tree) changes. The template renders it via
239
+ * `<component :is="rootVNode">` — Vue accepts a VNode object as the
240
+ * target of `:is`, which lets us drive the render from a render
241
+ * function while keeping the SFC `<template>` block as the single
242
+ * mount point.
243
+ */
244
+ const rootVNode = computed<VNode>(() => {
245
+ const t = tree.value;
246
+ if (t === undefined) {
247
+ const err = normalised.value.error;
248
+ if (err !== undefined) {
249
+ if (props.onError !== undefined) {
250
+ props.onError(err);
251
+ return h("span", { style: { display: "none" } });
252
+ }
253
+ throw err;
254
+ }
255
+ return h("span", { style: { display: "none" } });
256
+ }
257
+ const renderChild = makeRenderChild(0, rootPath.value);
258
+ return vueRenderField(
259
+ t,
260
+ effectiveValue.value,
261
+ handleChange,
262
+ props.resolver ?? contextResolver,
263
+ renderChild,
264
+ rootPath.value,
265
+ props.widgets,
266
+ contextWidgets,
267
+ 0
268
+ );
269
+ });
270
+ </script>
271
+
272
+ <template>
273
+ <VNodeHost :node="rootVNode" />
274
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * `<SchemaErrorBoundary>` — Vue counterpart of the React
4
+ * `SchemaErrorBoundary`.
5
+ *
6
+ * Uses Vue 3's `onErrorCaptured` lifecycle hook
7
+ * (https://vuejs.org/api/composition-api-lifecycle.html#onerrorcaptured)
8
+ * to catch render-time errors thrown by any descendant — including
9
+ * the dispatcher-wrapped `SchemaRenderError` from theme adapters that
10
+ * throw inside their render function. Returning `false` from
11
+ * `onErrorCaptured` halts further propagation up the component tree.
12
+ *
13
+ * The fallback slot is invoked with the captured error and a `reset`
14
+ * callback. Calling `reset()` clears the captured error so the
15
+ * children re-render (e.g. after fixing a bad schema prop).
16
+ *
17
+ * Like the React boundary, this captures render-time and lifecycle
18
+ * errors but NOT errors thrown from event handlers (Vue routes those
19
+ * through a separate `errorHandler` on the app instance) or async
20
+ * code that escapes the component tree.
21
+ *
22
+ * @group Components
23
+ */
24
+ import { onErrorCaptured, ref } from "vue";
25
+
26
+ const captured = ref<Error | undefined>(undefined);
27
+
28
+ defineSlots<{
29
+ /**
30
+ * Default slot — rendered when no error has been captured.
31
+ */
32
+ default(): unknown;
33
+ /**
34
+ * Fallback slot — invoked with the captured error and a `reset`
35
+ * callback. Use it to render an error UI; call `reset` to clear
36
+ * the captured state and let the children re-render.
37
+ */
38
+ fallback(props: { error: Error; reset: () => void }): unknown;
39
+ }>();
40
+
41
+ onErrorCaptured((err) => {
42
+ captured.value =
43
+ err instanceof Error ? err : new Error("Unknown render error");
44
+ return false;
45
+ });
46
+
47
+ function reset(): void {
48
+ captured.value = undefined;
49
+ }
50
+ </script>
51
+
52
+ <template>
53
+ <slot
54
+ v-if="captured !== undefined"
55
+ name="fallback"
56
+ :error="captured"
57
+ :reset="reset"
58
+ />
59
+ <slot v-else />
60
+ </template>