page.mdx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. export const metadata = { title: "Registry" }
  2. # Registry
  3. A registry maps your [catalog](/docs/catalog) definitions to platform-specific implementations. The catalog defines *what* AI can generate — the registry provides the *how*.
  4. What a registry contains depends on the schema you use. Each package defines its own schema, which determines the shape of both the catalog and the registry.
  5. - **`@json-render/react`** — Components (React elements) and action handlers
  6. - **`@json-render/react-native`** — Components (React Native elements) and action handlers
  7. - **`@json-render/remotion`** — Clip components, transitions, and effects
  8. ## @json-render/react
  9. ### defineRegistry
  10. Use `defineRegistry` to create a type-safe registry from your catalog. Pass your components, actions, or both:
  11. ```tsx
  12. import { defineRegistry } from '@json-render/react';
  13. import { myCatalog } from './catalog';
  14. export const { registry, handlers, executeAction } = defineRegistry(myCatalog, {
  15. components: {
  16. Card: ({ props, children }) => (
  17. <div className="card">
  18. <h2>{props.title}</h2>
  19. {props.description && <p>{props.description}</p>}
  20. {children}
  21. </div>
  22. ),
  23. Button: ({ props, emit }) => (
  24. <button onClick={() => emit("press")}>
  25. {props.label}
  26. </button>
  27. ),
  28. },
  29. actions: {
  30. submit_form: async (params, setState) => {
  31. const res = await fetch('/api/submit', {
  32. method: 'POST',
  33. body: JSON.stringify(params),
  34. });
  35. const result = await res.json();
  36. setState((prev) => ({ ...prev, formResult: result }));
  37. },
  38. export_data: async (params) => {
  39. const blob = await generateExport(params.format);
  40. downloadBlob(blob, `export.${params.format}`);
  41. },
  42. },
  43. });
  44. ```
  45. The returned object contains:
  46. - `registry` — component registry for `<Renderer />`
  47. - `handlers` — factory for ActionProvider-compatible handlers
  48. - `executeAction` — imperative action dispatch (for use outside the React tree)
  49. ### Component Props
  50. Each component receives a `ComponentContext` object:
  51. ```typescript
  52. interface ComponentContext {
  53. props: T; // Type-safe props from your catalog
  54. children?: React.ReactNode; // Rendered children (for slot components)
  55. emit: (event: string) => void; // Emit a named event (always defined)
  56. on: (event: string) => EventHandle; // Get event handle with metadata
  57. loading?: boolean; // Whether the renderer is in a loading state
  58. bindings?: Record<string, string>; // State paths from $bindState/$bindItem expressions
  59. }
  60. interface EventHandle {
  61. emit: () => void; // Fire the event
  62. shouldPreventDefault: boolean; // Whether any binding requested preventDefault
  63. bound: boolean; // Whether any handler is bound
  64. }
  65. ```
  66. Props are automatically inferred from your catalog, so `props.title` is typed as `string` if your catalog defines it that way.
  67. Use `emit("press")` for simple event firing. Use `on("click")` when you need to inspect event metadata:
  68. ```tsx
  69. Link: ({ props, on }) => {
  70. const click = on("click");
  71. return (
  72. <a
  73. href={props.href}
  74. onClick={(e) => {
  75. if (click.shouldPreventDefault) e.preventDefault();
  76. click.emit();
  77. }}
  78. >
  79. {props.label}
  80. </a>
  81. );
  82. },
  83. ```
  84. #### Using `bindings` for two-way binding
  85. When a spec uses `{ "$bindState": "/path" }` or `{ "$bindItem": "field" }` on a prop, the renderer resolves the **value** into `props` and provides the **write-back path** in `bindings`. Use the `useBoundProp` hook to wire both together:
  86. ```tsx
  87. import { useBoundProp, defineRegistry } from '@json-render/react';
  88. // Inside your registry:
  89. TextInput: ({ props, bindings }) => {
  90. const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
  91. return (
  92. <input
  93. value={value ?? ""}
  94. onChange={(e) => setValue(e.target.value)}
  95. />
  96. );
  97. },
  98. ```
  99. `useBoundProp` returns `[resolvedValue, setter]`. The setter writes to the bound state path. If no binding exists (the prop is a literal), the setter is a no-op.
  100. ### Action Handlers
  101. Instead of AI generating arbitrary code, it declares *intent* by name. Your application provides the implementation. This is a core guardrail.
  102. Actions are declared in your [catalog](/docs/catalog). The `@json-render/react` schema supports an `actions` key where you define what operations AI can trigger:
  103. ```typescript
  104. import { defineCatalog } from '@json-render/core';
  105. import { schema } from '@json-render/react';
  106. import { z } from 'zod';
  107. const catalog = defineCatalog(schema, {
  108. components: { /* ... */ },
  109. actions: {
  110. submit_form: {
  111. params: z.object({
  112. formId: z.string(),
  113. }),
  114. description: 'Submit a form',
  115. },
  116. export_data: {
  117. params: z.object({
  118. format: z.enum(['csv', 'pdf', 'json']),
  119. }),
  120. },
  121. navigate: {
  122. params: z.object({
  123. url: z.string(),
  124. }),
  125. },
  126. },
  127. });
  128. ```
  129. Action handlers receive `(params, setState, state)` and are defined inside `defineRegistry`:
  130. ```tsx
  131. export const { handlers, executeAction } = defineRegistry(catalog, {
  132. actions: {
  133. submit_form: async (params, setState) => {
  134. const response = await fetch('/api/submit', {
  135. method: 'POST',
  136. body: JSON.stringify({ formId: params.formId }),
  137. });
  138. const result = await response.json();
  139. setState((prev) => ({ ...prev, formResult: result }));
  140. },
  141. export_data: async (params) => {
  142. const blob = await generateExport(params.format);
  143. downloadBlob(blob, `export.${params.format}`);
  144. },
  145. navigate: (params) => {
  146. window.location.href = params.url;
  147. },
  148. },
  149. });
  150. ```
  151. ### Data Binding
  152. Most data binding is handled automatically by the renderer — `$state`, `$item`, and `$index` expressions in props are resolved before your component receives them. See the [Data Binding](/docs/data-binding) guide for the full reference.
  153. For two-way binding (form inputs), use `{ "$bindState": "/path" }` on the natural value prop (or `{ "$bindItem": "field" }` inside repeat scopes). The renderer provides a `bindings` map with the state path for each bound prop. Use `useBoundProp` to get `[value, setValue]`:
  154. ```tsx
  155. import { useBoundProp } from '@json-render/react';
  156. // Inside defineRegistry components:
  157. Input: ({ props, bindings }) => {
  158. const [value, setValue] = useBoundProp<string>(
  159. props.value,
  160. bindings?.value
  161. );
  162. return (
  163. <input
  164. value={value ?? ''}
  165. onChange={(e) => setValue(e.target.value)}
  166. placeholder={props.placeholder}
  167. />
  168. );
  169. },
  170. ```
  171. For read-only state access (e.g. displaying a value from state), use `$state` expressions in props — they are resolved before the component receives them. For custom logic, use `useStateStore` and `getByPath` from `@json-render/core`.
  172. ### Using the Renderer
  173. Wire everything together with providers and the `<Renderer />` component:
  174. ```tsx
  175. import { useMemo, useRef } from 'react';
  176. import {
  177. Renderer,
  178. StateProvider,
  179. VisibilityProvider,
  180. ActionProvider,
  181. } from '@json-render/react';
  182. import { registry, handlers } from './registry';
  183. function App({ spec, state, setState }) {
  184. const stateRef = useRef(state);
  185. const setStateRef = useRef(setState);
  186. stateRef.current = state;
  187. setStateRef.current = setState;
  188. const actionHandlers = useMemo(
  189. () => handlers(() => setStateRef.current, () => stateRef.current),
  190. [],
  191. );
  192. return (
  193. <StateProvider initialState={state}>
  194. <VisibilityProvider>
  195. <ActionProvider handlers={actionHandlers}>
  196. <Renderer spec={spec} registry={registry} />
  197. </ActionProvider>
  198. </VisibilityProvider>
  199. </StateProvider>
  200. );
  201. }
  202. ```
  203. ## @json-render/react-native
  204. `@json-render/react-native` uses the same `defineRegistry` API. The only difference is that components return React Native elements instead of HTML:
  205. ```tsx
  206. import { defineRegistry } from '@json-render/react-native';
  207. import { View, Text, Pressable } from 'react-native';
  208. export const { registry } = defineRegistry(catalog, {
  209. components: {
  210. Card: ({ props, children }) => (
  211. <View style={styles.card}>
  212. <Text style={styles.title}>{props.title}</Text>
  213. {children}
  214. </View>
  215. ),
  216. Button: ({ props, emit }) => (
  217. <Pressable onPress={() => emit("press")}>
  218. <Text>{props.label}</Text>
  219. </Pressable>
  220. ),
  221. },
  222. });
  223. ```
  224. See the [@json-render/react-native API reference](/docs/api/react-native) for the full API.
  225. ## @json-render/remotion
  226. `@json-render/remotion` takes a different approach. Instead of `defineRegistry`, it uses a plain component registry with built-in standard components for video production:
  227. ```tsx
  228. import { Renderer, standardComponents } from '@json-render/remotion';
  229. // Use the standard components directly
  230. <Renderer spec={timelineSpec} components={standardComponents} />
  231. // Or extend with your own
  232. const components = {
  233. ...standardComponents,
  234. CustomSlide: ({ clip }) => <AbsoluteFill>{/* ... */}</AbsoluteFill>,
  235. };
  236. ```
  237. The Remotion schema also supports `transitions` and `effects` in the catalog rather than actions.
  238. See the [@json-render/remotion API reference](/docs/api/remotion) for the full API.
  239. ## Next
  240. Learn about [data binding](/docs/data-binding) for dynamic values.