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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-instance id-prefix derivation, parallel to
|
|
3
|
+
* `react/SchemaComponent.tsx`'s {@link sanitisePrefix}.
|
|
4
|
+
*
|
|
5
|
+
* Vue 3.5 introduced `useId()` for stable per-instance ids inside
|
|
6
|
+
* `setup()`. The helper below normalises whatever Vue returns into a
|
|
7
|
+
* DOM-id-safe string (no colons, parens, or other punctuation that
|
|
8
|
+
* breaks CSS selectors) so the result composes with the canonical
|
|
9
|
+
* `sc-` prefix from `core/cssClasses.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Older Vue versions (before 3.5) do not expose `useId()`; the SFCs that
|
|
12
|
+
* consume the helper therefore prefer it when available and fall back
|
|
13
|
+
* to a monotonic counter scoped to the module. Both branches produce
|
|
14
|
+
* deterministic, sanitised prefixes.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useId as vueUseId } from "vue";
|
|
18
|
+
|
|
19
|
+
let fallbackCounter = 0;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sanitise a raw id value into a DOM-id-safe prefix. Mirrors the
|
|
23
|
+
* sanitiser used by the React adapter so prefixes derived from
|
|
24
|
+
* `useId()` survive every CSS selector and `aria-controls` reference.
|
|
25
|
+
*/
|
|
26
|
+
export function sanitisePrefix(value: string): string {
|
|
27
|
+
const sanitised = value
|
|
28
|
+
.replace(/[^a-zA-Z0-9_]+/g, "-")
|
|
29
|
+
.replace(/^-+|-+$/g, "");
|
|
30
|
+
if (sanitised.length === 0) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Cannot derive a DOM-safe id prefix from "${value}". Pass an explicit idPrefix prop.`
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return sanitised;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Append a child path suffix to a parent path. When the suffix is
|
|
40
|
+
* omitted (e.g. transparent wrappers like union options), the parent
|
|
41
|
+
* path is returned unchanged so the child inherits the parent's id.
|
|
42
|
+
*
|
|
43
|
+
* Mirror of `react/SchemaComponent.tsx` `joinPath` — bracketed array
|
|
44
|
+
* indices like `[0]` append directly so `tags` + `[0]` becomes
|
|
45
|
+
* `tags[0]`, matching the canonical form used by `html/a11y.ts` and
|
|
46
|
+
* `core/fieldPath.ts`.
|
|
47
|
+
*/
|
|
48
|
+
export function joinPath(parent: string, suffix: string | undefined): string {
|
|
49
|
+
if (suffix === undefined || suffix.length === 0) return parent;
|
|
50
|
+
if (parent.length === 0) return suffix;
|
|
51
|
+
if (suffix.startsWith("[")) return `${parent}${suffix}`;
|
|
52
|
+
return `${parent}.${suffix}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Derive the per-instance id prefix used by the Vue SFCs.
|
|
57
|
+
*
|
|
58
|
+
* Prefers Vue 3.5+ `useId()` (which guarantees per-component
|
|
59
|
+
* uniqueness across SSR/CSR hydration). Older Vue runtimes expose
|
|
60
|
+
* `useId` as a function returning `undefined` or omit it entirely
|
|
61
|
+
* — the helper falls back to a monotonic counter scoped to this
|
|
62
|
+
* module so the resulting prefix is still deterministic and
|
|
63
|
+
* collision-free within a single render tree.
|
|
64
|
+
*
|
|
65
|
+
* Must be called from within a `setup()` invocation: Vue's `useId`
|
|
66
|
+
* only resolves inside an active component instance.
|
|
67
|
+
*/
|
|
68
|
+
export function deriveIdPrefix(explicit: string | undefined): string {
|
|
69
|
+
if (explicit !== undefined) return sanitisePrefix(explicit);
|
|
70
|
+
// Vue's `useId` is typed to always return a string in 3.5+. The
|
|
71
|
+
// runtime check guards older Vue versions where the export may
|
|
72
|
+
// be `undefined`.
|
|
73
|
+
const raw = typeof vueUseId === "function" ? vueUseId() : undefined;
|
|
74
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
75
|
+
return sanitisePrefix(raw);
|
|
76
|
+
}
|
|
77
|
+
fallbackCounter += 1;
|
|
78
|
+
return `sc-vue-${String(fallbackCounter)}`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vue-flavoured wrapper around the framework-agnostic
|
|
3
|
+
* `dispatchRenderField` from `core/renderField.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Constructs a Vue-shaped {@link DispatchConfig} (widget lookup against
|
|
6
|
+
* the instance → context → global chain, recursion sentinel as a Vue
|
|
7
|
+
* `<fieldset>` {@link VNode}, fallback as a `<span>`-wrapped value) and
|
|
8
|
+
* forwards the call. Used by the `<SchemaComponent>` and `<SchemaView>`
|
|
9
|
+
* SFCs and exported so other Vue surfaces (future API operation
|
|
10
|
+
* components, etc.) can dispatch into the same fallback chain.
|
|
11
|
+
*
|
|
12
|
+
* The widget-lookup contract matches the React adapter exactly:
|
|
13
|
+
* instance map first, then context map, then global registry. The
|
|
14
|
+
* dispatcher itself remains agnostic to how widget maps are scoped —
|
|
15
|
+
* the resolution chain is expressed here in the `lookupWidget`
|
|
16
|
+
* closure.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { h, isVNode, type VNode } from "vue";
|
|
20
|
+
import { dispatchRenderField } from "../core/renderField.ts";
|
|
21
|
+
import type { WalkedField } from "../core/types.ts";
|
|
22
|
+
import { EM_DASH } from "../core/cssClasses.ts";
|
|
23
|
+
import { getVueRenderFunction, mergeVueResolvers } from "./resolver.ts";
|
|
24
|
+
import { headlessVueResolver } from "./headless.ts";
|
|
25
|
+
import type {
|
|
26
|
+
VueComponentResolver,
|
|
27
|
+
VueRenderProps,
|
|
28
|
+
VueWidgetMap,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
import { lookupGlobalWidget } from "./widget.ts";
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the {@link VueRenderProps} object handed to a Vue render
|
|
34
|
+
* function or widget. Mirrors `buildRenderProps` in `core/renderer.ts`
|
|
35
|
+
* but emits Vue's `VNode`-returning `renderChild` signature directly so
|
|
36
|
+
* the dispatcher does not need to cross-cast between React and Vue
|
|
37
|
+
* shapes at the resolver boundary.
|
|
38
|
+
*/
|
|
39
|
+
function buildVueRenderProps(
|
|
40
|
+
tree: WalkedField,
|
|
41
|
+
value: unknown,
|
|
42
|
+
onChange: (next: unknown) => void,
|
|
43
|
+
renderChild: VueRenderProps["renderChild"],
|
|
44
|
+
path: string
|
|
45
|
+
): VueRenderProps {
|
|
46
|
+
const isReadOnly = tree.editability === "presentation";
|
|
47
|
+
const isWriteOnly = tree.editability === "input";
|
|
48
|
+
const props: VueRenderProps = {
|
|
49
|
+
value,
|
|
50
|
+
onChange,
|
|
51
|
+
readOnly: isReadOnly,
|
|
52
|
+
writeOnly: isWriteOnly,
|
|
53
|
+
meta: tree.meta,
|
|
54
|
+
constraints: tree.constraints,
|
|
55
|
+
path,
|
|
56
|
+
tree,
|
|
57
|
+
renderChild,
|
|
58
|
+
};
|
|
59
|
+
if (tree.examples !== undefined) props.examples = tree.examples;
|
|
60
|
+
return props;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Render a single walked field through the resolved widget / resolver
|
|
65
|
+
* / headless pipeline.
|
|
66
|
+
*
|
|
67
|
+
* Thin Vue-flavoured wrapper around {@link dispatchRenderField}: it
|
|
68
|
+
* constructs a Vue-shaped {@link DispatchConfig} and returns the
|
|
69
|
+
* dispatcher's {@link VNode} output.
|
|
70
|
+
*
|
|
71
|
+
* @param tree - The walked field tree node to render.
|
|
72
|
+
* @param value - The current value at this position.
|
|
73
|
+
* @param onChange - Callback invoked when the field emits a change. For
|
|
74
|
+
* read-only renders (e.g. `<SchemaView>`) pass a noop.
|
|
75
|
+
* @param userResolver - User-supplied resolver, or `undefined` to use
|
|
76
|
+
* the headless resolver alone.
|
|
77
|
+
* @param renderChild - Recursive child renderer threaded through
|
|
78
|
+
* the {@link VueRenderProps} `renderChild` field.
|
|
79
|
+
* @param path - Dot-separated structural path; non-empty.
|
|
80
|
+
* @param instanceWidgets - Per-instance widget map (highest priority).
|
|
81
|
+
* @param contextWidgets - Context-scoped widget map (middle priority).
|
|
82
|
+
* @param depth - Recursion depth used by the depth cap in
|
|
83
|
+
* {@link dispatchRenderField}.
|
|
84
|
+
*/
|
|
85
|
+
export function vueRenderField(
|
|
86
|
+
tree: WalkedField,
|
|
87
|
+
value: unknown,
|
|
88
|
+
onChange: (v: unknown) => void,
|
|
89
|
+
userResolver: VueComponentResolver | undefined,
|
|
90
|
+
renderChild: VueRenderProps["renderChild"],
|
|
91
|
+
path: string,
|
|
92
|
+
instanceWidgets?: VueWidgetMap,
|
|
93
|
+
contextWidgets?: VueWidgetMap,
|
|
94
|
+
depth = 0
|
|
95
|
+
): VNode {
|
|
96
|
+
if (path.length === 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
"vueRenderField requires a non-empty path. Pass the root path " +
|
|
99
|
+
"(derived from `idPrefix` or `useId()`) for the root field, " +
|
|
100
|
+
"and use renderChild's pathSuffix to derive child paths."
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Build the merged resolver once per dispatch — user overrides on
|
|
105
|
+
// top of the headless fallback, mirroring the historic React
|
|
106
|
+
// behaviour.
|
|
107
|
+
const resolver: VueComponentResolver =
|
|
108
|
+
userResolver !== undefined
|
|
109
|
+
? mergeVueResolvers(userResolver, headlessVueResolver)
|
|
110
|
+
: headlessVueResolver;
|
|
111
|
+
|
|
112
|
+
return dispatchRenderField<VueRenderProps, VNode, VueComponentResolver>({
|
|
113
|
+
tree,
|
|
114
|
+
value,
|
|
115
|
+
path,
|
|
116
|
+
depth,
|
|
117
|
+
resolver,
|
|
118
|
+
config: {
|
|
119
|
+
buildProps: (fieldTree, fieldPath) =>
|
|
120
|
+
buildVueRenderProps(
|
|
121
|
+
fieldTree,
|
|
122
|
+
value,
|
|
123
|
+
onChange,
|
|
124
|
+
renderChild,
|
|
125
|
+
fieldPath
|
|
126
|
+
),
|
|
127
|
+
lookupRenderFn: (type, mergedResolver) =>
|
|
128
|
+
getVueRenderFunction(type, mergedResolver),
|
|
129
|
+
// Widget lookup follows the canonical Vue resolution
|
|
130
|
+
// order: instance → context → global. Pulled out as a
|
|
131
|
+
// closure so the dispatcher remains agnostic to how
|
|
132
|
+
// widget maps are scoped.
|
|
133
|
+
lookupWidget: (name) =>
|
|
134
|
+
instanceWidgets?.get(name) ??
|
|
135
|
+
contextWidgets?.get(name) ??
|
|
136
|
+
lookupGlobalWidget(name),
|
|
137
|
+
recursionSentinel: (fieldTree) => {
|
|
138
|
+
const label =
|
|
139
|
+
typeof fieldTree.meta.description === "string"
|
|
140
|
+
? fieldTree.meta.description
|
|
141
|
+
: "schema";
|
|
142
|
+
return h("fieldset", undefined, [
|
|
143
|
+
h("em", undefined, `↻ ${label} (recursive)`),
|
|
144
|
+
]);
|
|
145
|
+
},
|
|
146
|
+
fallback: (_fieldTree, fieldValue) => {
|
|
147
|
+
if (fieldValue === undefined || fieldValue === null)
|
|
148
|
+
return h("span", undefined, EM_DASH);
|
|
149
|
+
return h(
|
|
150
|
+
"span",
|
|
151
|
+
undefined,
|
|
152
|
+
typeof fieldValue === "string"
|
|
153
|
+
? fieldValue
|
|
154
|
+
: JSON.stringify(fieldValue)
|
|
155
|
+
);
|
|
156
|
+
},
|
|
157
|
+
coerceResult: (result, step) => {
|
|
158
|
+
if (step === "widget") {
|
|
159
|
+
if (result === undefined || result === null)
|
|
160
|
+
return undefined;
|
|
161
|
+
if (isVNode(result)) return result;
|
|
162
|
+
// Widget returned a value but not in a Vue-renderable
|
|
163
|
+
// shape — wrap it in a span so the output remains a
|
|
164
|
+
// valid VNode rather than falling through to the
|
|
165
|
+
// resolver.
|
|
166
|
+
if (
|
|
167
|
+
typeof result === "string" ||
|
|
168
|
+
typeof result === "number"
|
|
169
|
+
)
|
|
170
|
+
return h("span", undefined, String(result));
|
|
171
|
+
return h("span");
|
|
172
|
+
}
|
|
173
|
+
if (result === undefined || result === null)
|
|
174
|
+
return h("span", { style: { display: "none" } });
|
|
175
|
+
if (isVNode(result)) return result;
|
|
176
|
+
if (typeof result === "string" || typeof result === "number")
|
|
177
|
+
return h("span", undefined, String(result));
|
|
178
|
+
return undefined;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|