blocks-renderer 0.1.1 → 0.1.2
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 +13 -0
- package/adapter.d.ts +14 -0
- package/adapter.js +36 -0
- package/bindings.d.ts +6 -0
- package/bindings.js +12 -0
- package/context.d.ts +24 -0
- package/context.js +36 -0
- package/esm/adapter.js +33 -0
- package/esm/bindings.js +5 -0
- package/esm/context.js +31 -0
- package/esm/index.js +6 -0
- package/esm/registry.js +24 -0
- package/esm/renderer.js +85 -0
- package/esm/types.js +1 -0
- package/esm/unknown-block.js +10 -0
- package/index.d.ts +10 -0
- package/index.js +24 -0
- package/package.json +14 -32
- package/registry.d.ts +12 -0
- package/registry.js +30 -0
- package/renderer.d.ts +23 -0
- package/renderer.js +89 -0
- package/types.d.ts +32 -0
- package/types.js +2 -0
- package/unknown-block.d.ts +9 -0
- package/unknown-block.js +13 -0
- package/dist/index.cjs +0 -214
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -106
- package/dist/index.d.ts +0 -106
- package/dist/index.js +0 -196
- package/dist/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
# blocks-renderer
|
|
2
2
|
|
|
3
|
+
<p align="center" width="100%">
|
|
4
|
+
<img height="250" src="https://raw.githubusercontent.com/constructive-io/constructive/refs/heads/main/assets/outline-logo.svg" />
|
|
5
|
+
</p>
|
|
6
|
+
|
|
3
7
|
React adapter for `blocks-schema` UI documents: a recursive
|
|
4
8
|
renderer with layerable widget registries, binding resolution, form state, and
|
|
5
9
|
a visible unknown-block fallback.
|
|
6
10
|
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add blocks-renderer blocks-schema react
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Built with `makage` and published from `dist`, so deep imports resolve without an
|
|
18
|
+
exports map: `blocks-renderer/registry`, `blocks-renderer/bindings`.
|
|
19
|
+
|
|
7
20
|
## Usage
|
|
8
21
|
|
|
9
22
|
```tsx
|
package/adapter.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { UIDocument } from 'blocks-schema';
|
|
2
|
+
import { type RendererAdapter } from 'json-renderer';
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
import type { BlockComponent, RendererContextValue } from './types';
|
|
5
|
+
/**
|
|
6
|
+
* `blocks-renderer` as an explicit {@link RendererAdapter}: node type resolves to
|
|
7
|
+
* a React component, output is a React element, and an unsatisfied type renders
|
|
8
|
+
* {@link UnknownBlock}.
|
|
9
|
+
*
|
|
10
|
+
* The React components ({@link DocumentRenderer}, {@link BlockRenderer}) remain
|
|
11
|
+
* the ergonomic entry point; this object states the contract they satisfy so a
|
|
12
|
+
* second adapter has something to conform to.
|
|
13
|
+
*/
|
|
14
|
+
export declare const reactAdapter: RendererAdapter<BlockComponent, ReactNode, UIDocument, RendererContextValue>;
|
package/adapter.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.reactAdapter = void 0;
|
|
5
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
6
|
+
const json_renderer_1 = require("json-renderer");
|
|
7
|
+
const context_1 = require("./context");
|
|
8
|
+
const renderer_1 = require("./renderer");
|
|
9
|
+
const unknown_block_1 = require("./unknown-block");
|
|
10
|
+
/**
|
|
11
|
+
* `blocks-renderer` as an explicit {@link RendererAdapter}: node type resolves to
|
|
12
|
+
* a React component, output is a React element, and an unsatisfied type renders
|
|
13
|
+
* {@link UnknownBlock}.
|
|
14
|
+
*
|
|
15
|
+
* The React components ({@link DocumentRenderer}, {@link BlockRenderer}) remain
|
|
16
|
+
* the ergonomic entry point; this object states the contract they satisfy so a
|
|
17
|
+
* second adapter has something to conform to.
|
|
18
|
+
*/
|
|
19
|
+
exports.reactAdapter = {
|
|
20
|
+
name: 'blocks-renderer/react',
|
|
21
|
+
resolve(type, context) {
|
|
22
|
+
return (0, json_renderer_1.resolveNode)(context.registry, type);
|
|
23
|
+
},
|
|
24
|
+
resolveProps(node, context) {
|
|
25
|
+
return (0, json_renderer_1.resolveNodeProps)(node, context.scope);
|
|
26
|
+
},
|
|
27
|
+
renderNode(node, context) {
|
|
28
|
+
return (0, jsx_runtime_1.jsx)(context_1.RendererProvider, { value: context, children: (0, jsx_runtime_1.jsx)(renderer_1.BlockRenderer, { node: node }) });
|
|
29
|
+
},
|
|
30
|
+
renderUnknown(node) {
|
|
31
|
+
return (0, jsx_runtime_1.jsx)(unknown_block_1.UnknownBlock, { node: node });
|
|
32
|
+
},
|
|
33
|
+
renderDocument(document, context) {
|
|
34
|
+
return ((0, jsx_runtime_1.jsx)(context_1.RendererProvider, { value: { ...context, document }, children: (0, jsx_runtime_1.jsx)(renderer_1.BlockRenderer, { node: document.page }) }));
|
|
35
|
+
},
|
|
36
|
+
};
|
package/bindings.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binding resolution is framework-agnostic, so it lives in `json-renderer`.
|
|
3
|
+
* These re-exports keep `blocks-renderer/bindings` a stable import path.
|
|
4
|
+
*/
|
|
5
|
+
export { composeScope, readPath, resolveBinding, resolveNodeProps } from 'json-renderer';
|
|
6
|
+
export type { BindingScope } from 'json-renderer';
|
package/bindings.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveNodeProps = exports.resolveBinding = exports.readPath = exports.composeScope = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Binding resolution is framework-agnostic, so it lives in `json-renderer`.
|
|
6
|
+
* These re-exports keep `blocks-renderer/bindings` a stable import path.
|
|
7
|
+
*/
|
|
8
|
+
var json_renderer_1 = require("json-renderer");
|
|
9
|
+
Object.defineProperty(exports, "composeScope", { enumerable: true, get: function () { return json_renderer_1.composeScope; } });
|
|
10
|
+
Object.defineProperty(exports, "readPath", { enumerable: true, get: function () { return json_renderer_1.readPath; } });
|
|
11
|
+
Object.defineProperty(exports, "resolveBinding", { enumerable: true, get: function () { return json_renderer_1.resolveBinding; } });
|
|
12
|
+
Object.defineProperty(exports, "resolveNodeProps", { enumerable: true, get: function () { return json_renderer_1.resolveNodeProps; } });
|
package/context.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { RendererContextValue } from './types';
|
|
3
|
+
export declare function RendererProvider({ children, value }: {
|
|
4
|
+
children: ReactNode;
|
|
5
|
+
value: RendererContextValue;
|
|
6
|
+
}): import("react").JSX.Element;
|
|
7
|
+
export declare function useRenderer(): RendererContextValue;
|
|
8
|
+
/**
|
|
9
|
+
* Resolved props plus the value/error wiring for a field node. Widget
|
|
10
|
+
* implementations use this instead of reaching into the document themselves.
|
|
11
|
+
*/
|
|
12
|
+
export declare function useBlockField(name: string | undefined): {
|
|
13
|
+
value: undefined;
|
|
14
|
+
error: undefined;
|
|
15
|
+
setValue: () => void;
|
|
16
|
+
setError: () => void;
|
|
17
|
+
mode: import("./types").RenderMode;
|
|
18
|
+
} | {
|
|
19
|
+
value: unknown;
|
|
20
|
+
error: string;
|
|
21
|
+
setValue: (value: unknown) => void;
|
|
22
|
+
setError: (error: string | null) => void;
|
|
23
|
+
mode: import("./types").RenderMode;
|
|
24
|
+
};
|
package/context.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.RendererProvider = RendererProvider;
|
|
5
|
+
exports.useRenderer = useRenderer;
|
|
6
|
+
exports.useBlockField = useBlockField;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const RendererContext = (0, react_1.createContext)(null);
|
|
10
|
+
function RendererProvider({ children, value }) {
|
|
11
|
+
return (0, jsx_runtime_1.jsx)(RendererContext.Provider, { value: value, children: children });
|
|
12
|
+
}
|
|
13
|
+
function useRenderer() {
|
|
14
|
+
const context = (0, react_1.useContext)(RendererContext);
|
|
15
|
+
if (!context) {
|
|
16
|
+
throw new Error('useRenderer must be used within a RendererProvider (or a DocumentRenderer)');
|
|
17
|
+
}
|
|
18
|
+
return context;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Resolved props plus the value/error wiring for a field node. Widget
|
|
22
|
+
* implementations use this instead of reaching into the document themselves.
|
|
23
|
+
*/
|
|
24
|
+
function useBlockField(name) {
|
|
25
|
+
const { values, errors, setValue, setError, mode } = useRenderer();
|
|
26
|
+
if (!name) {
|
|
27
|
+
return { value: undefined, error: undefined, setValue: () => { }, setError: () => { }, mode };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
value: values[name],
|
|
31
|
+
error: errors[name],
|
|
32
|
+
setValue: (value) => setValue(name, value),
|
|
33
|
+
setError: (error) => setError(name, error),
|
|
34
|
+
mode,
|
|
35
|
+
};
|
|
36
|
+
}
|
package/esm/adapter.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { resolveNode, resolveNodeProps } from 'json-renderer';
|
|
4
|
+
import { RendererProvider } from './context';
|
|
5
|
+
import { BlockRenderer } from './renderer';
|
|
6
|
+
import { UnknownBlock } from './unknown-block';
|
|
7
|
+
/**
|
|
8
|
+
* `blocks-renderer` as an explicit {@link RendererAdapter}: node type resolves to
|
|
9
|
+
* a React component, output is a React element, and an unsatisfied type renders
|
|
10
|
+
* {@link UnknownBlock}.
|
|
11
|
+
*
|
|
12
|
+
* The React components ({@link DocumentRenderer}, {@link BlockRenderer}) remain
|
|
13
|
+
* the ergonomic entry point; this object states the contract they satisfy so a
|
|
14
|
+
* second adapter has something to conform to.
|
|
15
|
+
*/
|
|
16
|
+
export const reactAdapter = {
|
|
17
|
+
name: 'blocks-renderer/react',
|
|
18
|
+
resolve(type, context) {
|
|
19
|
+
return resolveNode(context.registry, type);
|
|
20
|
+
},
|
|
21
|
+
resolveProps(node, context) {
|
|
22
|
+
return resolveNodeProps(node, context.scope);
|
|
23
|
+
},
|
|
24
|
+
renderNode(node, context) {
|
|
25
|
+
return _jsx(RendererProvider, { value: context, children: _jsx(BlockRenderer, { node: node }) });
|
|
26
|
+
},
|
|
27
|
+
renderUnknown(node) {
|
|
28
|
+
return _jsx(UnknownBlock, { node: node });
|
|
29
|
+
},
|
|
30
|
+
renderDocument(document, context) {
|
|
31
|
+
return (_jsx(RendererProvider, { value: { ...context, document }, children: _jsx(BlockRenderer, { node: document.page }) }));
|
|
32
|
+
},
|
|
33
|
+
};
|
package/esm/bindings.js
ADDED
package/esm/context.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from 'react';
|
|
4
|
+
const RendererContext = createContext(null);
|
|
5
|
+
export function RendererProvider({ children, value }) {
|
|
6
|
+
return _jsx(RendererContext.Provider, { value: value, children: children });
|
|
7
|
+
}
|
|
8
|
+
export function useRenderer() {
|
|
9
|
+
const context = useContext(RendererContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error('useRenderer must be used within a RendererProvider (or a DocumentRenderer)');
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolved props plus the value/error wiring for a field node. Widget
|
|
17
|
+
* implementations use this instead of reaching into the document themselves.
|
|
18
|
+
*/
|
|
19
|
+
export function useBlockField(name) {
|
|
20
|
+
const { values, errors, setValue, setError, mode } = useRenderer();
|
|
21
|
+
if (!name) {
|
|
22
|
+
return { value: undefined, error: undefined, setValue: () => { }, setError: () => { }, mode };
|
|
23
|
+
}
|
|
24
|
+
return {
|
|
25
|
+
value: values[name],
|
|
26
|
+
error: errors[name],
|
|
27
|
+
setValue: (value) => setValue(name, value),
|
|
28
|
+
setError: (error) => setError(name, error),
|
|
29
|
+
mode,
|
|
30
|
+
};
|
|
31
|
+
}
|
package/esm/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { reactAdapter } from './adapter';
|
|
2
|
+
export { composeScope, readPath, resolveBinding, resolveNodeProps } from './bindings';
|
|
3
|
+
export { RendererProvider, useBlockField, useRenderer } from './context';
|
|
4
|
+
export { composeRegistry, missingTypes, registeredTypes, resolveBlock } from './registry';
|
|
5
|
+
export { BlockRenderer, DocumentRenderer } from './renderer';
|
|
6
|
+
export { UnknownBlock } from './unknown-block';
|
package/esm/registry.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registry layering for the React adapter: `json-renderer`'s generic registry
|
|
3
|
+
* ops, typed over React block components.
|
|
4
|
+
*/
|
|
5
|
+
import { composeRegistry as composeNodeRegistry, missingTypes as missingRegistryTypes, registeredTypes as registeredNodeTypes, resolveHandler, } from 'json-renderer';
|
|
6
|
+
/**
|
|
7
|
+
* Layer registries left-to-right, later layers winning. This is how a host
|
|
8
|
+
* customizes rendering: base primitives, then an app registry, then per-document
|
|
9
|
+
* overrides — no forking of the renderer, and no single global map.
|
|
10
|
+
*/
|
|
11
|
+
export function composeRegistry(...layers) {
|
|
12
|
+
return composeNodeRegistry(...layers);
|
|
13
|
+
}
|
|
14
|
+
export function resolveBlock(registry, type) {
|
|
15
|
+
return resolveHandler(registry, type);
|
|
16
|
+
}
|
|
17
|
+
/** Node types the registry can render, sorted for stable output. */
|
|
18
|
+
export function registeredTypes(registry) {
|
|
19
|
+
return registeredNodeTypes(registry);
|
|
20
|
+
}
|
|
21
|
+
/** Node types a document uses that no registry layer satisfies. */
|
|
22
|
+
export function missingTypes(registry, usedTypes) {
|
|
23
|
+
return missingRegistryTypes(registry, usedTypes);
|
|
24
|
+
}
|
package/esm/renderer.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { collectDefaultValues, collectFieldConstraints, collectFieldNames, validateField, } from 'blocks-schema';
|
|
4
|
+
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
5
|
+
import { resolveNodeProps } from './bindings';
|
|
6
|
+
import { RendererProvider, useRenderer } from './context';
|
|
7
|
+
import { UnknownBlock } from './unknown-block';
|
|
8
|
+
/**
|
|
9
|
+
* Renders one node and, recursively, its children: resolve the node type in the
|
|
10
|
+
* registry, resolve bindings against the current scope, render children first,
|
|
11
|
+
* fall back to {@link UnknownBlock}.
|
|
12
|
+
*/
|
|
13
|
+
export function BlockRenderer({ node }) {
|
|
14
|
+
const { registry, scope } = useRenderer();
|
|
15
|
+
const Component = registry[node.type];
|
|
16
|
+
if (!Component) {
|
|
17
|
+
return _jsx(UnknownBlock, { node: node });
|
|
18
|
+
}
|
|
19
|
+
const children = node.children?.length
|
|
20
|
+
? node.children.map((child) => _jsx(BlockRenderer, { node: child }, child.key))
|
|
21
|
+
: undefined;
|
|
22
|
+
return (_jsx(Component, { node: node, props: resolveNodeProps(node, scope), children: children }));
|
|
23
|
+
}
|
|
24
|
+
export function DocumentRenderer({ document, registry, initialValues, scope: externalScope, onSubmit, onChange, onAction, mode = 'preview', className, }) {
|
|
25
|
+
const defaults = useMemo(() => collectDefaultValues(document.page), [document]);
|
|
26
|
+
const constraints = useMemo(() => collectFieldConstraints(document.page), [document]);
|
|
27
|
+
const [values, setValues] = useState(() => ({ ...defaults, ...initialValues }));
|
|
28
|
+
const [errors, setErrors] = useState({});
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setValues((previous) => ({ ...defaults, ...initialValues, ...previous }));
|
|
31
|
+
}, [defaults, initialValues]);
|
|
32
|
+
const setError = useCallback((name, error) => {
|
|
33
|
+
setErrors((previous) => {
|
|
34
|
+
if (error)
|
|
35
|
+
return { ...previous, [name]: error };
|
|
36
|
+
if (!(name in previous))
|
|
37
|
+
return previous;
|
|
38
|
+
const next = { ...previous };
|
|
39
|
+
delete next[name];
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
42
|
+
}, []);
|
|
43
|
+
const setValue = useCallback((name, value) => {
|
|
44
|
+
setValues((previous) => {
|
|
45
|
+
const next = { ...previous, [name]: value };
|
|
46
|
+
onChange?.(next);
|
|
47
|
+
return next;
|
|
48
|
+
});
|
|
49
|
+
const field = constraints[name];
|
|
50
|
+
if (field) {
|
|
51
|
+
setError(name, validateField(value, field.constraints, field.required));
|
|
52
|
+
}
|
|
53
|
+
}, [onChange, constraints, setError]);
|
|
54
|
+
const handleAction = useCallback((action, event) => {
|
|
55
|
+
if (event === 'submit' && onSubmit) {
|
|
56
|
+
const failures = {};
|
|
57
|
+
for (const name of collectFieldNames(document.page)) {
|
|
58
|
+
const field = constraints[name];
|
|
59
|
+
if (!field)
|
|
60
|
+
continue;
|
|
61
|
+
const error = validateField(values[name], field.constraints, field.required);
|
|
62
|
+
if (error)
|
|
63
|
+
failures[name] = error;
|
|
64
|
+
}
|
|
65
|
+
if (Object.keys(failures).length > 0) {
|
|
66
|
+
setErrors(failures);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
onSubmit(values);
|
|
70
|
+
}
|
|
71
|
+
onAction?.(action, event);
|
|
72
|
+
}, [onSubmit, onAction, values, document.page, constraints]);
|
|
73
|
+
const contextValue = useMemo(() => ({
|
|
74
|
+
document,
|
|
75
|
+
registry,
|
|
76
|
+
mode,
|
|
77
|
+
values,
|
|
78
|
+
errors,
|
|
79
|
+
setValue,
|
|
80
|
+
setError,
|
|
81
|
+
scope: { ...externalScope, values, ...values },
|
|
82
|
+
onAction: handleAction,
|
|
83
|
+
}), [document, registry, mode, values, errors, setValue, setError, externalScope, handleAction]);
|
|
84
|
+
return (_jsx(RendererProvider, { value: contextValue, children: _jsx("div", { className: className, children: _jsx(BlockRenderer, { node: document.page }) }) }));
|
|
85
|
+
}
|
package/esm/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Rendered when no registry layer satisfies a node type. A document may name
|
|
5
|
+
* blocks a given host has not installed, so an unknown type is a visible gap,
|
|
6
|
+
* never a thrown render.
|
|
7
|
+
*/
|
|
8
|
+
export function UnknownBlock({ node }) {
|
|
9
|
+
return (_jsxs("div", { "data-block-unknown": node.type, role: "note", children: ["Unknown block: ", node.type] }));
|
|
10
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { reactAdapter } from './adapter';
|
|
2
|
+
export { composeScope, readPath, resolveBinding, resolveNodeProps } from './bindings';
|
|
3
|
+
export type { BindingScope } from './bindings';
|
|
4
|
+
export { RendererProvider, useBlockField, useRenderer } from './context';
|
|
5
|
+
export { composeRegistry, missingTypes, registeredTypes, resolveBlock } from './registry';
|
|
6
|
+
export { BlockRenderer, DocumentRenderer } from './renderer';
|
|
7
|
+
export type { DocumentRendererProps } from './renderer';
|
|
8
|
+
export { UnknownBlock } from './unknown-block';
|
|
9
|
+
export type { BlockComponent, BlockProps, BlockRegistry, RenderMode, RendererContextValue } from './types';
|
|
10
|
+
export type { NodeResolution, RenderContext, RendererAdapter, UnknownNodePolicy, } from 'json-renderer';
|
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UnknownBlock = exports.DocumentRenderer = exports.BlockRenderer = exports.resolveBlock = exports.registeredTypes = exports.missingTypes = exports.composeRegistry = exports.useRenderer = exports.useBlockField = exports.RendererProvider = exports.resolveNodeProps = exports.resolveBinding = exports.readPath = exports.composeScope = exports.reactAdapter = void 0;
|
|
4
|
+
var adapter_1 = require("./adapter");
|
|
5
|
+
Object.defineProperty(exports, "reactAdapter", { enumerable: true, get: function () { return adapter_1.reactAdapter; } });
|
|
6
|
+
var bindings_1 = require("./bindings");
|
|
7
|
+
Object.defineProperty(exports, "composeScope", { enumerable: true, get: function () { return bindings_1.composeScope; } });
|
|
8
|
+
Object.defineProperty(exports, "readPath", { enumerable: true, get: function () { return bindings_1.readPath; } });
|
|
9
|
+
Object.defineProperty(exports, "resolveBinding", { enumerable: true, get: function () { return bindings_1.resolveBinding; } });
|
|
10
|
+
Object.defineProperty(exports, "resolveNodeProps", { enumerable: true, get: function () { return bindings_1.resolveNodeProps; } });
|
|
11
|
+
var context_1 = require("./context");
|
|
12
|
+
Object.defineProperty(exports, "RendererProvider", { enumerable: true, get: function () { return context_1.RendererProvider; } });
|
|
13
|
+
Object.defineProperty(exports, "useBlockField", { enumerable: true, get: function () { return context_1.useBlockField; } });
|
|
14
|
+
Object.defineProperty(exports, "useRenderer", { enumerable: true, get: function () { return context_1.useRenderer; } });
|
|
15
|
+
var registry_1 = require("./registry");
|
|
16
|
+
Object.defineProperty(exports, "composeRegistry", { enumerable: true, get: function () { return registry_1.composeRegistry; } });
|
|
17
|
+
Object.defineProperty(exports, "missingTypes", { enumerable: true, get: function () { return registry_1.missingTypes; } });
|
|
18
|
+
Object.defineProperty(exports, "registeredTypes", { enumerable: true, get: function () { return registry_1.registeredTypes; } });
|
|
19
|
+
Object.defineProperty(exports, "resolveBlock", { enumerable: true, get: function () { return registry_1.resolveBlock; } });
|
|
20
|
+
var renderer_1 = require("./renderer");
|
|
21
|
+
Object.defineProperty(exports, "BlockRenderer", { enumerable: true, get: function () { return renderer_1.BlockRenderer; } });
|
|
22
|
+
Object.defineProperty(exports, "DocumentRenderer", { enumerable: true, get: function () { return renderer_1.DocumentRenderer; } });
|
|
23
|
+
var unknown_block_1 = require("./unknown-block");
|
|
24
|
+
Object.defineProperty(exports, "UnknownBlock", { enumerable: true, get: function () { return unknown_block_1.UnknownBlock; } });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "blocks-renderer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Recursive React renderer for Constructive Blocks JSON UI documents, with layerable widget registries",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,41 +14,24 @@
|
|
|
14
14
|
"url": "https://github.com/constructive-io/blocks/issues"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
|
-
"access": "public"
|
|
17
|
+
"access": "public",
|
|
18
|
+
"directory": "dist"
|
|
18
19
|
},
|
|
19
|
-
"type": "module",
|
|
20
20
|
"sideEffects": false,
|
|
21
|
-
"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"types": "./dist/index.d.ts",
|
|
25
|
-
"default": "./dist/index.js"
|
|
26
|
-
},
|
|
27
|
-
"require": {
|
|
28
|
-
"types": "./dist/index.d.cts",
|
|
29
|
-
"default": "./dist/index.cjs"
|
|
30
|
-
}
|
|
31
|
-
},
|
|
32
|
-
"./package.json": "./package.json"
|
|
33
|
-
},
|
|
34
|
-
"main": "./dist/index.cjs",
|
|
35
|
-
"module": "./dist/index.js",
|
|
36
|
-
"types": "./dist/index.d.ts",
|
|
37
|
-
"files": [
|
|
38
|
-
"dist",
|
|
39
|
-
"LICENSE",
|
|
40
|
-
"README.md"
|
|
41
|
-
],
|
|
21
|
+
"main": "index.js",
|
|
22
|
+
"module": "esm/index.js",
|
|
23
|
+
"types": "index.d.ts",
|
|
42
24
|
"scripts": {
|
|
43
|
-
"build": "
|
|
44
|
-
"dev": "
|
|
45
|
-
"lint:types": "tsc --noEmit",
|
|
25
|
+
"build": "makage clean && makage build-ts && makage assets",
|
|
26
|
+
"build:dev": "makage clean && makage build-ts --dev && makage assets",
|
|
27
|
+
"lint:types": "tsc --noEmit -p tsconfig.lint.json",
|
|
46
28
|
"test": "vitest run",
|
|
47
29
|
"test:watch": "vitest",
|
|
48
|
-
"clean": "
|
|
30
|
+
"clean": "makage clean"
|
|
49
31
|
},
|
|
50
32
|
"dependencies": {
|
|
51
|
-
"blocks-schema": "
|
|
33
|
+
"blocks-schema": "workspace:^",
|
|
34
|
+
"json-renderer": "workspace:^"
|
|
52
35
|
},
|
|
53
36
|
"peerDependencies": {
|
|
54
37
|
"react": "^18.0.0 || ^19.0.0"
|
|
@@ -56,14 +39,13 @@
|
|
|
56
39
|
"devDependencies": {
|
|
57
40
|
"@types/react": "^19.2.7",
|
|
58
41
|
"@types/react-dom": "^19.2.3",
|
|
42
|
+
"makage": "^0.6.0",
|
|
59
43
|
"react": "^19.2.3",
|
|
60
44
|
"react-dom": "^19.2.3",
|
|
61
|
-
"tsup": "^8.5.1",
|
|
62
45
|
"typescript": "^5.9.3",
|
|
63
46
|
"vitest": "^3.2.4"
|
|
64
47
|
},
|
|
65
48
|
"engines": {
|
|
66
49
|
"node": ">=24.0.0"
|
|
67
|
-
}
|
|
68
|
-
"gitHead": "b0fd1126b68b23543a51949c1e847c4d820863d3"
|
|
50
|
+
}
|
|
69
51
|
}
|
package/registry.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlockComponent, BlockRegistry } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* Layer registries left-to-right, later layers winning. This is how a host
|
|
4
|
+
* customizes rendering: base primitives, then an app registry, then per-document
|
|
5
|
+
* overrides — no forking of the renderer, and no single global map.
|
|
6
|
+
*/
|
|
7
|
+
export declare function composeRegistry(...layers: (BlockRegistry | undefined)[]): BlockRegistry;
|
|
8
|
+
export declare function resolveBlock(registry: BlockRegistry, type: string): BlockComponent | undefined;
|
|
9
|
+
/** Node types the registry can render, sorted for stable output. */
|
|
10
|
+
export declare function registeredTypes(registry: BlockRegistry): string[];
|
|
11
|
+
/** Node types a document uses that no registry layer satisfies. */
|
|
12
|
+
export declare function missingTypes(registry: BlockRegistry, usedTypes: Iterable<string>): string[];
|
package/registry.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.composeRegistry = composeRegistry;
|
|
4
|
+
exports.resolveBlock = resolveBlock;
|
|
5
|
+
exports.registeredTypes = registeredTypes;
|
|
6
|
+
exports.missingTypes = missingTypes;
|
|
7
|
+
/**
|
|
8
|
+
* Registry layering for the React adapter: `json-renderer`'s generic registry
|
|
9
|
+
* ops, typed over React block components.
|
|
10
|
+
*/
|
|
11
|
+
const json_renderer_1 = require("json-renderer");
|
|
12
|
+
/**
|
|
13
|
+
* Layer registries left-to-right, later layers winning. This is how a host
|
|
14
|
+
* customizes rendering: base primitives, then an app registry, then per-document
|
|
15
|
+
* overrides — no forking of the renderer, and no single global map.
|
|
16
|
+
*/
|
|
17
|
+
function composeRegistry(...layers) {
|
|
18
|
+
return (0, json_renderer_1.composeRegistry)(...layers);
|
|
19
|
+
}
|
|
20
|
+
function resolveBlock(registry, type) {
|
|
21
|
+
return (0, json_renderer_1.resolveHandler)(registry, type);
|
|
22
|
+
}
|
|
23
|
+
/** Node types the registry can render, sorted for stable output. */
|
|
24
|
+
function registeredTypes(registry) {
|
|
25
|
+
return (0, json_renderer_1.registeredTypes)(registry);
|
|
26
|
+
}
|
|
27
|
+
/** Node types a document uses that no registry layer satisfies. */
|
|
28
|
+
function missingTypes(registry, usedTypes) {
|
|
29
|
+
return (0, json_renderer_1.missingTypes)(registry, usedTypes);
|
|
30
|
+
}
|
package/renderer.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { type UIAction, type UIDocument, type UINode } from 'blocks-schema';
|
|
2
|
+
import type { BlockRegistry, RenderMode } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Renders one node and, recursively, its children: resolve the node type in the
|
|
5
|
+
* registry, resolve bindings against the current scope, render children first,
|
|
6
|
+
* fall back to {@link UnknownBlock}.
|
|
7
|
+
*/
|
|
8
|
+
export declare function BlockRenderer({ node }: {
|
|
9
|
+
node: UINode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
export interface DocumentRendererProps {
|
|
12
|
+
document: UIDocument;
|
|
13
|
+
registry: BlockRegistry;
|
|
14
|
+
initialValues?: Record<string, unknown>;
|
|
15
|
+
/** Extra data for binding expressions, e.g. `{ row, user, params }`. */
|
|
16
|
+
scope?: Record<string, unknown>;
|
|
17
|
+
onSubmit?: (values: Record<string, unknown>) => void;
|
|
18
|
+
onChange?: (values: Record<string, unknown>) => void;
|
|
19
|
+
onAction?: (action: UIAction, event: string) => void;
|
|
20
|
+
mode?: RenderMode;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function DocumentRenderer({ document, registry, initialValues, scope: externalScope, onSubmit, onChange, onAction, mode, className, }: DocumentRendererProps): import("react").JSX.Element;
|
package/renderer.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.BlockRenderer = BlockRenderer;
|
|
5
|
+
exports.DocumentRenderer = DocumentRenderer;
|
|
6
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
7
|
+
const blocks_schema_1 = require("blocks-schema");
|
|
8
|
+
const react_1 = require("react");
|
|
9
|
+
const bindings_1 = require("./bindings");
|
|
10
|
+
const context_1 = require("./context");
|
|
11
|
+
const unknown_block_1 = require("./unknown-block");
|
|
12
|
+
/**
|
|
13
|
+
* Renders one node and, recursively, its children: resolve the node type in the
|
|
14
|
+
* registry, resolve bindings against the current scope, render children first,
|
|
15
|
+
* fall back to {@link UnknownBlock}.
|
|
16
|
+
*/
|
|
17
|
+
function BlockRenderer({ node }) {
|
|
18
|
+
const { registry, scope } = (0, context_1.useRenderer)();
|
|
19
|
+
const Component = registry[node.type];
|
|
20
|
+
if (!Component) {
|
|
21
|
+
return (0, jsx_runtime_1.jsx)(unknown_block_1.UnknownBlock, { node: node });
|
|
22
|
+
}
|
|
23
|
+
const children = node.children?.length
|
|
24
|
+
? node.children.map((child) => (0, jsx_runtime_1.jsx)(BlockRenderer, { node: child }, child.key))
|
|
25
|
+
: undefined;
|
|
26
|
+
return ((0, jsx_runtime_1.jsx)(Component, { node: node, props: (0, bindings_1.resolveNodeProps)(node, scope), children: children }));
|
|
27
|
+
}
|
|
28
|
+
function DocumentRenderer({ document, registry, initialValues, scope: externalScope, onSubmit, onChange, onAction, mode = 'preview', className, }) {
|
|
29
|
+
const defaults = (0, react_1.useMemo)(() => (0, blocks_schema_1.collectDefaultValues)(document.page), [document]);
|
|
30
|
+
const constraints = (0, react_1.useMemo)(() => (0, blocks_schema_1.collectFieldConstraints)(document.page), [document]);
|
|
31
|
+
const [values, setValues] = (0, react_1.useState)(() => ({ ...defaults, ...initialValues }));
|
|
32
|
+
const [errors, setErrors] = (0, react_1.useState)({});
|
|
33
|
+
(0, react_1.useEffect)(() => {
|
|
34
|
+
setValues((previous) => ({ ...defaults, ...initialValues, ...previous }));
|
|
35
|
+
}, [defaults, initialValues]);
|
|
36
|
+
const setError = (0, react_1.useCallback)((name, error) => {
|
|
37
|
+
setErrors((previous) => {
|
|
38
|
+
if (error)
|
|
39
|
+
return { ...previous, [name]: error };
|
|
40
|
+
if (!(name in previous))
|
|
41
|
+
return previous;
|
|
42
|
+
const next = { ...previous };
|
|
43
|
+
delete next[name];
|
|
44
|
+
return next;
|
|
45
|
+
});
|
|
46
|
+
}, []);
|
|
47
|
+
const setValue = (0, react_1.useCallback)((name, value) => {
|
|
48
|
+
setValues((previous) => {
|
|
49
|
+
const next = { ...previous, [name]: value };
|
|
50
|
+
onChange?.(next);
|
|
51
|
+
return next;
|
|
52
|
+
});
|
|
53
|
+
const field = constraints[name];
|
|
54
|
+
if (field) {
|
|
55
|
+
setError(name, (0, blocks_schema_1.validateField)(value, field.constraints, field.required));
|
|
56
|
+
}
|
|
57
|
+
}, [onChange, constraints, setError]);
|
|
58
|
+
const handleAction = (0, react_1.useCallback)((action, event) => {
|
|
59
|
+
if (event === 'submit' && onSubmit) {
|
|
60
|
+
const failures = {};
|
|
61
|
+
for (const name of (0, blocks_schema_1.collectFieldNames)(document.page)) {
|
|
62
|
+
const field = constraints[name];
|
|
63
|
+
if (!field)
|
|
64
|
+
continue;
|
|
65
|
+
const error = (0, blocks_schema_1.validateField)(values[name], field.constraints, field.required);
|
|
66
|
+
if (error)
|
|
67
|
+
failures[name] = error;
|
|
68
|
+
}
|
|
69
|
+
if (Object.keys(failures).length > 0) {
|
|
70
|
+
setErrors(failures);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
onSubmit(values);
|
|
74
|
+
}
|
|
75
|
+
onAction?.(action, event);
|
|
76
|
+
}, [onSubmit, onAction, values, document.page, constraints]);
|
|
77
|
+
const contextValue = (0, react_1.useMemo)(() => ({
|
|
78
|
+
document,
|
|
79
|
+
registry,
|
|
80
|
+
mode,
|
|
81
|
+
values,
|
|
82
|
+
errors,
|
|
83
|
+
setValue,
|
|
84
|
+
setError,
|
|
85
|
+
scope: { ...externalScope, values, ...values },
|
|
86
|
+
onAction: handleAction,
|
|
87
|
+
}), [document, registry, mode, values, errors, setValue, setError, externalScope, handleAction]);
|
|
88
|
+
return ((0, jsx_runtime_1.jsx)(context_1.RendererProvider, { value: contextValue, children: (0, jsx_runtime_1.jsx)("div", { className: className, children: (0, jsx_runtime_1.jsx)(BlockRenderer, { node: document.page }) }) }));
|
|
89
|
+
}
|