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.
- package/README.md +41 -6
- package/dist/{SchemaComponent-B__6-5-E.d.mts → SchemaComponent-CRgCVDhz.d.mts} +27 -15
- package/dist/{SchemaComponent-BxzzsHsK.mjs → SchemaComponent-Cga5oJfP.mjs} +3 -3
- package/dist/core/renderField.mjs +41 -7
- package/dist/html/streamRenderers.d.mts +12 -3
- package/dist/html/streamRenderers.mjs +56 -10
- package/dist/lit/SchemaComponent.d.mts +2 -2
- package/dist/lit/SchemaComponent.mjs +1 -1
- package/dist/lit/SchemaField.d.mts +1 -1
- package/dist/lit/SchemaField.mjs +1 -1
- package/dist/lit/SchemaView.mjs +1 -1
- package/dist/lit/defaultResolver.mjs +1 -1
- package/dist/lit/registry.mjs +1 -1
- package/dist/preact/SchemaComponent.d.mts +1 -1
- package/dist/react/SchemaComponent.d.mts +1 -1
- package/dist/react/SchemaComponent.mjs +6 -6
- package/dist/react/SchemaView.d.mts +16 -11
- package/dist/react/SchemaView.mjs +2 -2
- package/dist/solid/SchemaComponent.d.mts +6 -6
- package/dist/solid/SchemaComponent.mjs +1 -1
- package/dist/solid/SchemaField.d.mts +3 -3
- package/dist/solid/SchemaField.mjs +1 -1
- package/dist/solid/SchemaView.d.mts +5 -5
- package/dist/solid/SchemaView.mjs +1 -1
- package/package.json +5 -3
- package/src/svelte/SchemaComponent.svelte +3 -3
- package/src/svelte/SchemaField.svelte +3 -3
- package/src/svelte/SchemaView.svelte +3 -3
- package/src/vue/SchemaComponent.vue +274 -0
- package/src/vue/SchemaErrorBoundary.vue +60 -0
- package/src/vue/SchemaField.vue +178 -0
- package/src/vue/SchemaProvider.vue +39 -0
- package/src/vue/SchemaView.vue +198 -0
- package/src/vue/VNodeHost.ts +32 -0
- package/src/vue/contexts.ts +116 -0
- package/src/vue/eventTargets.ts +35 -0
- package/src/vue/headless.ts +61 -0
- package/src/vue/idPrefix.ts +79 -0
- package/src/vue/renderField.ts +182 -0
- package/src/vue/renderers.ts +1297 -0
- package/src/vue/resolver.ts +45 -0
- package/src/vue/types.ts +140 -0
- package/src/vue/vue-shim.d.ts +25 -0
- 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,
|
|
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
|
-
|
|
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,
|
|
53
|
+
value?: InferSchemaValue<T, SchemaRef, Mode>;
|
|
54
54
|
/** Called when the value changes; receives the next value. */
|
|
55
|
-
onChange?: (value: InferSchemaValue<T,
|
|
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,
|
|
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,
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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,
|
|
20
|
+
interface SchemaViewProps<T = unknown, SchemaRef extends string | undefined = undefined, Mode extends SchemaIoSide = "output"> {
|
|
21
21
|
schema: RejectUnrepresentableZod<T>;
|
|
22
|
-
|
|
22
|
+
schemaRef?: SchemaRef;
|
|
23
23
|
io?: Mode;
|
|
24
|
-
value?: InferredValue<T,
|
|
25
|
-
fields?: InferFields<T,
|
|
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,
|
|
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.
|
|
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": "
|
|
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": "./
|
|
34
|
-
"import": "./
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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>
|