|
|
@@ -14,82 +14,86 @@ export default function RegistryPage() {
|
|
|
life.
|
|
|
</p>
|
|
|
|
|
|
- {/* Components Section */}
|
|
|
- <h2 className="text-xl font-semibold mt-12 mb-4">Component Registry</h2>
|
|
|
+ {/* defineRegistry */}
|
|
|
+ <h2 className="text-xl font-semibold mt-12 mb-4">defineRegistry</h2>
|
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
|
- Create a registry that maps catalog component types to React components:
|
|
|
+ Use <code>defineRegistry</code> to create a type-safe registry from your
|
|
|
+ catalog. Pass your components, actions, or both in a single call:
|
|
|
</p>
|
|
|
- <Code lang="tsx">{`import { useAction } from '@json-render/react';
|
|
|
-
|
|
|
-const registry = {
|
|
|
- Card: ({ props, children }) => (
|
|
|
- <div className="card">
|
|
|
- <h2>{props.title}</h2>
|
|
|
- {props.description && <p>{props.description}</p>}
|
|
|
- {children}
|
|
|
- </div>
|
|
|
- ),
|
|
|
-
|
|
|
- Button: ({ props }) => {
|
|
|
- const executeAction = useAction(props.action);
|
|
|
- return (
|
|
|
- <button onClick={() => executeAction({})}>
|
|
|
+ <Code lang="tsx">{`import { defineRegistry } from '@json-render/react';
|
|
|
+import { myCatalog } from './catalog';
|
|
|
+
|
|
|
+export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
|
|
|
+ components: {
|
|
|
+ Card: ({ props, children }) => (
|
|
|
+ <div className="card">
|
|
|
+ <h2>{props.title}</h2>
|
|
|
+ {props.description && <p>{props.description}</p>}
|
|
|
+ {children}
|
|
|
+ </div>
|
|
|
+ ),
|
|
|
+
|
|
|
+ Button: ({ props, onAction }) => (
|
|
|
+ <button onClick={() => onAction?.({ name: props.action })}>
|
|
|
{props.label}
|
|
|
</button>
|
|
|
- );
|
|
|
+ ),
|
|
|
},
|
|
|
-};`}</Code>
|
|
|
|
|
|
+ actions: {
|
|
|
+ submit_form: async (params, setData) => {
|
|
|
+ const res = await fetch('/api/submit', {
|
|
|
+ method: 'POST',
|
|
|
+ body: JSON.stringify(params),
|
|
|
+ });
|
|
|
+ const result = await res.json();
|
|
|
+ setData((prev) => ({ ...prev, formResult: result }));
|
|
|
+ },
|
|
|
+
|
|
|
+ export_data: async (params) => {
|
|
|
+ const blob = await generateExport(params.format);
|
|
|
+ downloadBlob(blob, \`export.\${params.format}\`);
|
|
|
+ },
|
|
|
+ },
|
|
|
+});`}</Code>
|
|
|
+
|
|
|
+ <p className="text-sm text-muted-foreground mt-4 mb-4">
|
|
|
+ The returned object contains:
|
|
|
+ </p>
|
|
|
+ <ul className="list-disc list-inside text-sm text-muted-foreground mb-4 space-y-1">
|
|
|
+ <li>
|
|
|
+ <code>registry</code> - component registry for{" "}
|
|
|
+ <code>{"<Renderer />"}</code>
|
|
|
+ </li>
|
|
|
+ <li>
|
|
|
+ <code>handlers</code> - factory for ActionProvider-compatible handlers
|
|
|
+ </li>
|
|
|
+ <li>
|
|
|
+ <code>executeAction</code> - imperative action dispatch (for use
|
|
|
+ outside the React tree)
|
|
|
+ </li>
|
|
|
+ </ul>
|
|
|
+
|
|
|
+ {/* Component Props */}
|
|
|
<h2 className="text-xl font-semibold mt-12 mb-4">Component Props</h2>
|
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
|
- Each component receives these props:
|
|
|
+ Each component in the registry receives a <code>ComponentContext</code>{" "}
|
|
|
+ object:
|
|
|
</p>
|
|
|
- <Code lang="typescript">{`interface ComponentProps<T = Record<string, unknown>> {
|
|
|
- props: T; // Component props from the spec
|
|
|
+ <Code lang="typescript">{`interface ComponentContext {
|
|
|
+ props: T; // Type-safe props from your catalog
|
|
|
children?: React.ReactNode; // Rendered children (for slot components)
|
|
|
-}
|
|
|
-
|
|
|
-// Type-safe props by extracting from your catalog
|
|
|
-type CardProps = ComponentProps<{
|
|
|
- title: string;
|
|
|
- description: string | null;
|
|
|
-}>;
|
|
|
-
|
|
|
-// Use hooks for actions and data within components
|
|
|
-import { useAction, useDataValue } from '@json-render/react';`}</Code>
|
|
|
+ onAction?: (action: ActionTrigger) => void; // Dispatch an action
|
|
|
+ loading?: boolean; // Whether the renderer is in a loading state
|
|
|
+}`}</Code>
|
|
|
|
|
|
- <h2 className="text-xl font-semibold mt-12 mb-4">Using Data Binding</h2>
|
|
|
- <p className="text-sm text-muted-foreground mb-4">
|
|
|
- Use hooks to read and write data:
|
|
|
+ <p className="text-sm text-muted-foreground mt-4 mb-4">
|
|
|
+ Props are automatically inferred from your catalog, so{" "}
|
|
|
+ <code>props.title</code> is typed as <code>string</code> if your catalog
|
|
|
+ defines it that way.
|
|
|
</p>
|
|
|
- <Code lang="tsx">{`import { useDataValue, useDataBinding } from '@json-render/react';
|
|
|
-
|
|
|
-const Metric = ({ props }) => {
|
|
|
- // Read-only value from data context
|
|
|
- const value = useDataValue(props.valuePath);
|
|
|
-
|
|
|
- return (
|
|
|
- <div className="metric">
|
|
|
- <span className="label">{props.label}</span>
|
|
|
- <span className="value">{formatValue(value)}</span>
|
|
|
- </div>
|
|
|
- );
|
|
|
-};
|
|
|
|
|
|
-const TextField = ({ props }) => {
|
|
|
- // Two-way binding to data context
|
|
|
- const [value, setValue] = useDataBinding(props.valuePath);
|
|
|
-
|
|
|
- return (
|
|
|
- <input
|
|
|
- value={value || ''}
|
|
|
- onChange={(e) => setValue(e.target.value)}
|
|
|
- placeholder={props.placeholder}
|
|
|
- />
|
|
|
- );
|
|
|
-};`}</Code>
|
|
|
-
|
|
|
- {/* Actions Section */}
|
|
|
+ {/* Action Handlers */}
|
|
|
<h2 className="text-xl font-semibold mt-12 mb-4">Action Handlers</h2>
|
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
|
Instead of AI generating arbitrary code, it declares <em>intent</em> by
|
|
|
@@ -117,9 +121,6 @@ const catalog = defineCatalog(schema, {
|
|
|
export_data: {
|
|
|
params: z.object({
|
|
|
format: z.enum(['csv', 'pdf', 'json']),
|
|
|
- filters: z.object({
|
|
|
- dateRange: z.string().nullable(),
|
|
|
- }).nullable(),
|
|
|
}),
|
|
|
},
|
|
|
navigate: {
|
|
|
@@ -130,78 +131,104 @@ const catalog = defineCatalog(schema, {
|
|
|
},
|
|
|
});`}</Code>
|
|
|
|
|
|
- <h3 className="text-lg font-medium mt-8 mb-3">ActionProvider</h3>
|
|
|
+ <h3 className="text-lg font-medium mt-8 mb-3">
|
|
|
+ Implementing Action Handlers
|
|
|
+ </h3>
|
|
|
<p className="text-sm text-muted-foreground mb-4">
|
|
|
- Provide action handlers to your app:
|
|
|
+ Action handlers receive <code>(params, setData, data)</code> and are
|
|
|
+ defined inside <code>defineRegistry</code>:
|
|
|
</p>
|
|
|
- <Code lang="tsx">{`import { ActionProvider } from '@json-render/react';
|
|
|
-
|
|
|
-function App() {
|
|
|
- const handlers = {
|
|
|
- submit_form: async (params) => {
|
|
|
+ <Code lang="tsx">{`export const { handlers, executeAction } = defineRegistry(catalog, {
|
|
|
+ actions: {
|
|
|
+ submit_form: async (params, setData) => {
|
|
|
const response = await fetch('/api/submit', {
|
|
|
method: 'POST',
|
|
|
body: JSON.stringify({ formId: params.formId }),
|
|
|
});
|
|
|
- return response.json();
|
|
|
+ const result = await response.json();
|
|
|
+ setData((prev) => ({ ...prev, formResult: result }));
|
|
|
},
|
|
|
-
|
|
|
+
|
|
|
export_data: async (params) => {
|
|
|
- const blob = await generateExport(params.format, params.filters);
|
|
|
+ const blob = await generateExport(params.format);
|
|
|
downloadBlob(blob, \`export.\${params.format}\`);
|
|
|
},
|
|
|
-
|
|
|
+
|
|
|
navigate: (params) => {
|
|
|
window.location.href = params.url;
|
|
|
},
|
|
|
- };
|
|
|
+ },
|
|
|
+});`}</Code>
|
|
|
|
|
|
- return (
|
|
|
- <ActionProvider handlers={handlers}>
|
|
|
- {/* Your UI */}
|
|
|
- </ActionProvider>
|
|
|
- );
|
|
|
-}`}</Code>
|
|
|
+ {/* Using Data Binding */}
|
|
|
+ <h2 className="text-xl font-semibold mt-12 mb-4">Using Data Binding</h2>
|
|
|
+ <p className="text-sm text-muted-foreground mb-4">
|
|
|
+ Use hooks inside your registry components to read and write data:
|
|
|
+ </p>
|
|
|
+ <Code lang="tsx">{`import { useData } from '@json-render/react';
|
|
|
+import { getByPath } from '@json-render/core';
|
|
|
|
|
|
- <h3 className="text-lg font-medium mt-8 mb-3">
|
|
|
- Using Actions in Components
|
|
|
- </h3>
|
|
|
- <Code lang="tsx">{`import { useAction } from '@json-render/react';
|
|
|
+// Inside defineRegistry components:
|
|
|
+
|
|
|
+Metric: ({ props }) => {
|
|
|
+ const { data } = useData();
|
|
|
+ const value = getByPath(data, props.valuePath);
|
|
|
|
|
|
-// Using the useAction hook (recommended)
|
|
|
-const Button = ({ props }) => {
|
|
|
- const executeAction = useAction(props.action);
|
|
|
-
|
|
|
return (
|
|
|
- <button onClick={() => executeAction({})}>
|
|
|
- {props.label}
|
|
|
- </button>
|
|
|
+ <div className="metric">
|
|
|
+ <span className="label">{props.label}</span>
|
|
|
+ <span className="value">{formatValue(value)}</span>
|
|
|
+ </div>
|
|
|
);
|
|
|
-};
|
|
|
+},
|
|
|
+
|
|
|
+TextField: ({ props }) => {
|
|
|
+ const { data, set } = useData();
|
|
|
+ const value = getByPath(data, props.valuePath) as string;
|
|
|
|
|
|
-// Or for standalone use
|
|
|
-function SubmitButton() {
|
|
|
- const submitForm = useAction('submit_form');
|
|
|
-
|
|
|
return (
|
|
|
- <button onClick={() => submitForm({ formId: 'contact' })}>
|
|
|
- Submit
|
|
|
- </button>
|
|
|
+ <input
|
|
|
+ value={value || ''}
|
|
|
+ onChange={(e) => set(props.valuePath, e.target.value)}
|
|
|
+ placeholder={props.placeholder}
|
|
|
+ />
|
|
|
);
|
|
|
-}`}</Code>
|
|
|
+},`}</Code>
|
|
|
|
|
|
{/* Renderer Section */}
|
|
|
<h2 className="text-xl font-semibold mt-12 mb-4">Using the Renderer</h2>
|
|
|
- <Code lang="tsx">{`import { Renderer, ActionProvider } from '@json-render/react';
|
|
|
+ <p className="text-sm text-muted-foreground mb-4">
|
|
|
+ Wire everything together with providers and the{" "}
|
|
|
+ <code>{"<Renderer />"}</code> component:
|
|
|
+ </p>
|
|
|
+ <Code lang="tsx">{`import { useMemo, useRef } from 'react';
|
|
|
+import {
|
|
|
+ Renderer,
|
|
|
+ DataProvider,
|
|
|
+ VisibilityProvider,
|
|
|
+ ActionProvider,
|
|
|
+} from '@json-render/react';
|
|
|
+import { registry, handlers } from './registry';
|
|
|
+
|
|
|
+function App({ spec, data, setData }) {
|
|
|
+ const dataRef = useRef(data);
|
|
|
+ const setDataRef = useRef(setData);
|
|
|
+ dataRef.current = data;
|
|
|
+ setDataRef.current = setData;
|
|
|
+
|
|
|
+ const actionHandlers = useMemo(
|
|
|
+ () => handlers(() => setDataRef.current, () => dataRef.current),
|
|
|
+ [],
|
|
|
+ );
|
|
|
|
|
|
-function App() {
|
|
|
return (
|
|
|
- <ActionProvider handlers={actionHandlers}>
|
|
|
- <Renderer
|
|
|
- spec={uiSpec}
|
|
|
- registry={registry}
|
|
|
- />
|
|
|
- </ActionProvider>
|
|
|
+ <DataProvider initialData={data}>
|
|
|
+ <VisibilityProvider>
|
|
|
+ <ActionProvider handlers={actionHandlers}>
|
|
|
+ <Renderer spec={spec} registry={registry} />
|
|
|
+ </ActionProvider>
|
|
|
+ </VisibilityProvider>
|
|
|
+ </DataProvider>
|
|
|
);
|
|
|
}`}</Code>
|
|
|
|