renderer.tsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748
  1. import React, {
  2. type ComponentType,
  3. type ErrorInfo,
  4. type ReactNode,
  5. useCallback,
  6. useMemo,
  7. } from "react";
  8. import type {
  9. UIElement,
  10. Spec,
  11. ActionBinding,
  12. Catalog,
  13. SchemaDefinition,
  14. LegacyCatalog,
  15. ComponentDefinition,
  16. } from "@json-render/core";
  17. import { resolveElementProps, getByPath } from "@json-render/core";
  18. import type {
  19. Components,
  20. Actions,
  21. ActionFn,
  22. SetState,
  23. StateModel,
  24. } from "./catalog-types";
  25. import { useIsVisible, useVisibility } from "./contexts/visibility";
  26. import { useActions } from "./contexts/actions";
  27. import { useStateStore } from "./contexts/state";
  28. import { StateProvider } from "./contexts/state";
  29. import { VisibilityProvider } from "./contexts/visibility";
  30. import { ActionProvider } from "./contexts/actions";
  31. import { ValidationProvider } from "./contexts/validation";
  32. import { ConfirmDialog } from "./contexts/actions";
  33. import { standardComponents } from "./components/standard";
  34. import {
  35. RepeatScopeProvider,
  36. useRepeatScope,
  37. rewriteRepeatTokens,
  38. } from "./contexts/repeat-scope";
  39. /**
  40. * Props passed to component renderers
  41. */
  42. export interface ComponentRenderProps<P = Record<string, unknown>> {
  43. /** The element being rendered */
  44. element: UIElement<string, P>;
  45. /** Rendered children */
  46. children?: ReactNode;
  47. /** Emit a named event. The renderer resolves the event to action binding(s) from the element's `on` field. */
  48. emit?: (event: string) => void;
  49. /** Whether the parent is loading */
  50. loading?: boolean;
  51. }
  52. /**
  53. * Component renderer type
  54. */
  55. export type ComponentRenderer<P = Record<string, unknown>> = ComponentType<
  56. ComponentRenderProps<P>
  57. >;
  58. /**
  59. * Registry of component renderers
  60. */
  61. export type ComponentRegistry = Record<string, ComponentRenderer<any>>;
  62. /**
  63. * Props for the Renderer component
  64. */
  65. export interface RendererProps {
  66. /** The UI spec to render */
  67. spec: Spec | null;
  68. /**
  69. * Component registry. If omitted, only standard components are used.
  70. * When provided, custom components are merged with (and override) standard components.
  71. */
  72. registry?: ComponentRegistry;
  73. /** Whether to include standard components (default: true) */
  74. includeStandard?: boolean;
  75. /** Whether the spec is currently loading/streaming */
  76. loading?: boolean;
  77. /** Fallback component for unknown types */
  78. fallback?: ComponentRenderer;
  79. }
  80. // ---------------------------------------------------------------------------
  81. // ElementErrorBoundary – catches rendering errors in individual elements so
  82. // a single bad component never crashes the whole page.
  83. // ---------------------------------------------------------------------------
  84. interface ElementErrorBoundaryProps {
  85. elementType: string;
  86. children: ReactNode;
  87. }
  88. interface ElementErrorBoundaryState {
  89. hasError: boolean;
  90. }
  91. class ElementErrorBoundary extends React.Component<
  92. ElementErrorBoundaryProps,
  93. ElementErrorBoundaryState
  94. > {
  95. constructor(props: ElementErrorBoundaryProps) {
  96. super(props);
  97. this.state = { hasError: false };
  98. }
  99. static getDerivedStateFromError(): ElementErrorBoundaryState {
  100. return { hasError: true };
  101. }
  102. componentDidCatch(error: Error, info: ErrorInfo) {
  103. console.error(
  104. `[json-render] Rendering error in <${this.props.elementType}>:`,
  105. error,
  106. info.componentStack,
  107. );
  108. }
  109. render() {
  110. if (this.state.hasError) {
  111. // Render nothing – the element silently disappears rather than
  112. // crashing the entire application.
  113. return null;
  114. }
  115. return this.props.children;
  116. }
  117. }
  118. /**
  119. * Element renderer component
  120. */
  121. function ElementRenderer({
  122. element,
  123. spec,
  124. registry,
  125. loading,
  126. fallback,
  127. }: {
  128. element: UIElement;
  129. spec: Spec;
  130. registry: ComponentRegistry;
  131. loading?: boolean;
  132. fallback?: ComponentRenderer;
  133. }) {
  134. const repeatScope = useRepeatScope();
  135. const { ctx } = useVisibility();
  136. const { execute } = useActions();
  137. // ---- Rewrite $item / $index tokens when inside a Repeat ----
  138. let effectiveElement = element;
  139. if (repeatScope) {
  140. const rewrittenProps = rewriteRepeatTokens(
  141. element.props,
  142. repeatScope.basePath,
  143. repeatScope.index,
  144. );
  145. const rewrittenVisible =
  146. element.visible !== undefined
  147. ? rewriteRepeatTokens(
  148. element.visible,
  149. repeatScope.basePath,
  150. repeatScope.index,
  151. )
  152. : element.visible;
  153. const rewrittenOn =
  154. element.on !== undefined
  155. ? rewriteRepeatTokens(
  156. element.on,
  157. repeatScope.basePath,
  158. repeatScope.index,
  159. )
  160. : element.on;
  161. if (
  162. rewrittenProps !== element.props ||
  163. rewrittenVisible !== element.visible ||
  164. rewrittenOn !== element.on
  165. ) {
  166. effectiveElement = {
  167. ...element,
  168. props: rewrittenProps as Record<string, unknown>,
  169. visible: rewrittenVisible as UIElement["visible"],
  170. on: rewrittenOn as UIElement["on"],
  171. };
  172. }
  173. }
  174. // Evaluate visibility (after token rewriting so paths are absolute)
  175. const isVisible = useIsVisible(effectiveElement.visible);
  176. // Create emit function that resolves events to action bindings.
  177. // Must be called before any early return to satisfy Rules of Hooks.
  178. const onBindings = effectiveElement.on;
  179. const emit = useCallback(
  180. (eventName: string) => {
  181. const binding = onBindings?.[eventName];
  182. if (!binding) return;
  183. const bindings = Array.isArray(binding) ? binding : [binding];
  184. for (const b of bindings) {
  185. execute(b);
  186. }
  187. },
  188. [onBindings, execute],
  189. );
  190. // Don't render if not visible
  191. if (!isVisible) {
  192. return null;
  193. }
  194. // Resolve dynamic prop expressions ($path, $cond/$then/$else)
  195. const resolvedProps = resolveElementProps(
  196. effectiveElement.props as Record<string, unknown>,
  197. ctx,
  198. );
  199. const resolvedElement =
  200. resolvedProps !== effectiveElement.props
  201. ? { ...effectiveElement, props: resolvedProps }
  202. : effectiveElement;
  203. // Get the component renderer
  204. const Component = registry[resolvedElement.type] ?? fallback;
  205. if (!Component) {
  206. console.warn(`No renderer for component type: ${resolvedElement.type}`);
  207. return null;
  208. }
  209. // ---- Render children (with repeat support) ----
  210. const children = resolvedElement.repeat ? (
  211. <RepeatChildren
  212. element={resolvedElement}
  213. spec={spec}
  214. registry={registry}
  215. loading={loading}
  216. fallback={fallback}
  217. />
  218. ) : (
  219. resolvedElement.children?.map((childKey) => {
  220. const childElement = spec.elements[childKey];
  221. if (!childElement) {
  222. if (!loading) {
  223. console.warn(
  224. `[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`,
  225. );
  226. }
  227. return null;
  228. }
  229. return (
  230. <ElementRenderer
  231. key={childKey}
  232. element={childElement}
  233. spec={spec}
  234. registry={registry}
  235. loading={loading}
  236. fallback={fallback}
  237. />
  238. );
  239. })
  240. );
  241. return (
  242. <ElementErrorBoundary elementType={resolvedElement.type}>
  243. <Component element={resolvedElement} emit={emit} loading={loading}>
  244. {children}
  245. </Component>
  246. </ElementErrorBoundary>
  247. );
  248. }
  249. // ---------------------------------------------------------------------------
  250. // RepeatChildren -- renders child elements once per item in a state array.
  251. // Used when an element has a `repeat` field.
  252. // ---------------------------------------------------------------------------
  253. function RepeatChildren({
  254. element,
  255. spec,
  256. registry,
  257. loading,
  258. fallback,
  259. }: {
  260. element: UIElement;
  261. spec: Spec;
  262. registry: ComponentRegistry;
  263. loading?: boolean;
  264. fallback?: ComponentRenderer;
  265. }) {
  266. const { state } = useStateStore();
  267. const repeat = element.repeat!;
  268. const statePath = repeat.path;
  269. const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];
  270. return (
  271. <>
  272. {items.map((item, index) => {
  273. // Use a stable key: prefer key field, fall back to index
  274. const key =
  275. repeat.key && typeof item === "object" && item !== null
  276. ? String((item as Record<string, unknown>)[repeat.key] ?? index)
  277. : String(index);
  278. return (
  279. <RepeatScopeProvider
  280. key={key}
  281. basePath={`${statePath}/${index}`}
  282. index={index}
  283. >
  284. {element.children?.map((childKey) => {
  285. const childElement = spec.elements[childKey];
  286. if (!childElement) {
  287. if (!loading) {
  288. console.warn(
  289. `[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`,
  290. );
  291. }
  292. return null;
  293. }
  294. return (
  295. <ElementRenderer
  296. key={childKey}
  297. element={childElement}
  298. spec={spec}
  299. registry={registry}
  300. loading={loading}
  301. fallback={fallback}
  302. />
  303. );
  304. })}
  305. </RepeatScopeProvider>
  306. );
  307. })}
  308. </>
  309. );
  310. }
  311. /**
  312. * Main renderer component.
  313. *
  314. * By default, standard React Native components are included.
  315. * Custom components in `registry` override standard ones with the same name.
  316. *
  317. * @example
  318. * ```tsx
  319. * // Use standard components only
  320. * <Renderer spec={spec} />
  321. *
  322. * // Add/override components
  323. * <Renderer spec={spec} registry={{ CustomCard: MyCard }} />
  324. *
  325. * // Disable standard components entirely
  326. * <Renderer spec={spec} registry={myRegistry} includeStandard={false} />
  327. * ```
  328. */
  329. export function Renderer({
  330. spec,
  331. registry: customRegistry,
  332. includeStandard = true,
  333. loading,
  334. fallback,
  335. }: RendererProps) {
  336. // Merge standard + custom components (custom overrides standard)
  337. const registry: ComponentRegistry = useMemo(
  338. () => ({
  339. ...(includeStandard ? standardComponents : {}),
  340. ...customRegistry,
  341. }),
  342. [customRegistry, includeStandard],
  343. );
  344. if (!spec || !spec.root) {
  345. return null;
  346. }
  347. const rootElement = spec.elements[spec.root];
  348. if (!rootElement) {
  349. return null;
  350. }
  351. return (
  352. <ElementRenderer
  353. element={rootElement}
  354. spec={spec}
  355. registry={registry}
  356. loading={loading}
  357. fallback={fallback}
  358. />
  359. );
  360. }
  361. // Re-export standard components and action handlers
  362. export {
  363. standardComponents,
  364. createStandardActionHandlers,
  365. } from "./components/standard";
  366. /**
  367. * Props for JSONUIProvider
  368. */
  369. export interface JSONUIProviderProps {
  370. /**
  371. * Component registry. If omitted, only standard components are used.
  372. * Custom components are merged with (and override) standard components.
  373. */
  374. registry?: ComponentRegistry;
  375. /** Initial state model */
  376. initialState?: Record<string, unknown>;
  377. /** Auth state */
  378. authState?: { isSignedIn: boolean; user?: Record<string, unknown> };
  379. /** Action handlers */
  380. actionHandlers?: Record<
  381. string,
  382. (params: Record<string, unknown>) => Promise<unknown> | unknown
  383. >;
  384. /** Navigation function */
  385. navigate?: (path: string) => void;
  386. /** Custom validation functions */
  387. validationFunctions?: Record<
  388. string,
  389. (value: unknown, args?: Record<string, unknown>) => boolean
  390. >;
  391. /** Callback when state changes */
  392. onStateChange?: (path: string, value: unknown) => void;
  393. children: ReactNode;
  394. }
  395. /**
  396. * Combined provider for all JSONUI contexts
  397. */
  398. export function JSONUIProvider({
  399. registry,
  400. initialState,
  401. authState,
  402. actionHandlers,
  403. navigate,
  404. validationFunctions,
  405. onStateChange,
  406. children,
  407. }: JSONUIProviderProps) {
  408. return (
  409. <StateProvider
  410. initialState={initialState}
  411. authState={authState}
  412. onStateChange={onStateChange}
  413. >
  414. <VisibilityProvider>
  415. <ActionProvider handlers={actionHandlers} navigate={navigate}>
  416. <ValidationProvider customFunctions={validationFunctions}>
  417. {children}
  418. <ConfirmationDialogManager />
  419. </ValidationProvider>
  420. </ActionProvider>
  421. </VisibilityProvider>
  422. </StateProvider>
  423. );
  424. }
  425. /**
  426. * Renders the confirmation dialog when needed
  427. */
  428. function ConfirmationDialogManager() {
  429. const { pendingConfirmation, confirm, cancel } = useActions();
  430. if (!pendingConfirmation?.action.confirm) {
  431. return null;
  432. }
  433. return (
  434. <ConfirmDialog
  435. confirm={pendingConfirmation.action.confirm}
  436. onConfirm={confirm}
  437. onCancel={cancel}
  438. />
  439. );
  440. }
  441. /**
  442. * Legacy helper to create a renderer component from a catalog
  443. * @deprecated Use createRenderer with the new catalog API instead
  444. */
  445. export function createRendererFromCatalog<
  446. C extends LegacyCatalog<Record<string, ComponentDefinition>>,
  447. >(
  448. _catalog: C,
  449. registry: ComponentRegistry,
  450. ): ComponentType<Omit<RendererProps, "registry">> {
  451. return function CatalogRenderer(props: Omit<RendererProps, "registry">) {
  452. return <Renderer {...props} registry={registry} />;
  453. };
  454. }
  455. // ============================================================================
  456. // defineRegistry
  457. // ============================================================================
  458. /**
  459. * Result returned by defineRegistry
  460. */
  461. export interface DefineRegistryResult {
  462. /** Component registry for `<Renderer registry={...} />` */
  463. registry: ComponentRegistry;
  464. /**
  465. * Create ActionProvider-compatible handlers.
  466. * Accepts getter functions so handlers always read the latest state/setState
  467. * (e.g. from React refs).
  468. */
  469. handlers: (
  470. getSetState: () => SetState | undefined,
  471. getState: () => StateModel,
  472. ) => Record<string, (params: Record<string, unknown>) => Promise<void>>;
  473. /**
  474. * Execute an action by name imperatively
  475. * (for use outside the React tree, e.g. initial state loading).
  476. */
  477. executeAction: (
  478. actionName: string,
  479. params: Record<string, unknown> | undefined,
  480. setState: SetState,
  481. state?: StateModel,
  482. ) => Promise<void>;
  483. }
  484. /**
  485. * Create a registry from a catalog with components and/or actions.
  486. *
  487. * @example
  488. * ```tsx
  489. * // Components only
  490. * const { registry } = defineRegistry(catalog, {
  491. * components: {
  492. * Card: ({ props, children }) => (
  493. * <View style={styles.card}><Text>{props.title}</Text>{children}</View>
  494. * ),
  495. * },
  496. * });
  497. *
  498. * // Actions only
  499. * const { handlers, executeAction } = defineRegistry(catalog, {
  500. * actions: {
  501. * viewCustomers: async (params, setState) => { ... },
  502. * },
  503. * });
  504. *
  505. * // Both
  506. * const { registry, handlers, executeAction } = defineRegistry(catalog, {
  507. * components: { ... },
  508. * actions: { ... },
  509. * });
  510. * ```
  511. */
  512. export function defineRegistry<C extends Catalog>(
  513. _catalog: C,
  514. options: {
  515. components?: Components<C>;
  516. actions?: Actions<C>;
  517. },
  518. ): DefineRegistryResult {
  519. // Build component registry
  520. const registry: ComponentRegistry = {};
  521. if (options.components) {
  522. for (const [name, componentFn] of Object.entries(options.components)) {
  523. registry[name] = ({
  524. element,
  525. children,
  526. emit,
  527. loading,
  528. }: ComponentRenderProps) => {
  529. return (componentFn as DefineRegistryComponentFn)({
  530. props: element.props,
  531. children,
  532. emit,
  533. loading,
  534. });
  535. };
  536. }
  537. }
  538. // Build action helpers
  539. const actionMap = options.actions
  540. ? (Object.entries(options.actions) as Array<
  541. [string, DefineRegistryActionFn]
  542. >)
  543. : [];
  544. const handlers = (
  545. getSetState: () => SetState | undefined,
  546. getState: () => StateModel,
  547. ): Record<string, (params: Record<string, unknown>) => Promise<void>> => {
  548. const result: Record<
  549. string,
  550. (params: Record<string, unknown>) => Promise<void>
  551. > = {};
  552. for (const [name, actionFn] of actionMap) {
  553. result[name] = async (params) => {
  554. const setState = getSetState();
  555. const state = getState();
  556. if (setState) {
  557. await actionFn(params, setState, state);
  558. }
  559. };
  560. }
  561. return result;
  562. };
  563. const executeAction = async (
  564. actionName: string,
  565. params: Record<string, unknown> | undefined,
  566. setState: SetState,
  567. state: StateModel = {},
  568. ): Promise<void> => {
  569. const entry = actionMap.find(([name]) => name === actionName);
  570. if (entry) {
  571. await entry[1](params, setState, state);
  572. } else {
  573. console.warn(`Unknown action: ${actionName}`);
  574. }
  575. };
  576. return { registry, handlers, executeAction };
  577. }
  578. /** @internal */
  579. type DefineRegistryComponentFn = (ctx: {
  580. props: unknown;
  581. children?: React.ReactNode;
  582. emit?: (event: string) => void;
  583. loading?: boolean;
  584. }) => React.ReactNode;
  585. /** @internal */
  586. type DefineRegistryActionFn = (
  587. params: Record<string, unknown> | undefined,
  588. setState: SetState,
  589. state: StateModel,
  590. ) => Promise<void>;
  591. // ============================================================================
  592. // NEW API
  593. // ============================================================================
  594. /**
  595. * Props for renderers created with createRenderer
  596. */
  597. export interface CreateRendererProps {
  598. /** The spec to render (AI-generated JSON) */
  599. spec: Spec | null;
  600. /** State context for dynamic values */
  601. state?: Record<string, unknown>;
  602. /** Action handler */
  603. onAction?: (actionName: string, params?: Record<string, unknown>) => void;
  604. /** Callback when state changes (e.g., from form inputs) */
  605. onStateChange?: (path: string, value: unknown) => void;
  606. /** Whether the spec is currently loading/streaming */
  607. loading?: boolean;
  608. /** Auth state for visibility conditions */
  609. authState?: { isSignedIn: boolean; user?: Record<string, unknown> };
  610. /** Fallback component for unknown types */
  611. fallback?: ComponentRenderer;
  612. }
  613. /**
  614. * Component map type - maps component names to React Native components
  615. */
  616. export type ComponentMap<
  617. TComponents extends Record<string, { props: unknown }>,
  618. > = {
  619. [K in keyof TComponents]: ComponentType<
  620. ComponentRenderProps<
  621. TComponents[K]["props"] extends { _output: infer O }
  622. ? O
  623. : Record<string, unknown>
  624. >
  625. >;
  626. };
  627. /**
  628. * Create a renderer from a catalog
  629. *
  630. * @example
  631. * ```typescript
  632. * const DashboardRenderer = createRenderer(dashboardCatalog, {
  633. * Card: ({ element, children }) => <View style={styles.card}>{children}</View>,
  634. * Metric: ({ element }) => <Text>{element.props.value}</Text>,
  635. * });
  636. *
  637. * // Usage
  638. * <DashboardRenderer spec={aiGeneratedSpec} state={state} />
  639. * ```
  640. */
  641. export function createRenderer<
  642. TDef extends SchemaDefinition,
  643. TCatalog extends { components: Record<string, { props: unknown }> },
  644. >(
  645. catalog: Catalog<TDef, TCatalog>,
  646. components: ComponentMap<TCatalog["components"]>,
  647. ): ComponentType<CreateRendererProps> {
  648. // Convert component map to registry
  649. const registry: ComponentRegistry =
  650. components as unknown as ComponentRegistry;
  651. // Return the renderer component
  652. return function CatalogRenderer({
  653. spec,
  654. state,
  655. onAction,
  656. onStateChange,
  657. loading,
  658. authState,
  659. fallback,
  660. }: CreateRendererProps) {
  661. // Wrap onAction to match internal API
  662. const actionHandlers = onAction
  663. ? {
  664. __default__: (params: Record<string, unknown>) => {
  665. const actionName = params.__actionName__ as string;
  666. const actionParams = params.__actionParams__ as Record<
  667. string,
  668. unknown
  669. >;
  670. return onAction(actionName, actionParams);
  671. },
  672. }
  673. : undefined;
  674. return (
  675. <StateProvider
  676. initialState={state}
  677. authState={authState}
  678. onStateChange={onStateChange}
  679. >
  680. <VisibilityProvider>
  681. <ActionProvider handlers={actionHandlers}>
  682. <ValidationProvider>
  683. <Renderer
  684. spec={spec}
  685. registry={registry}
  686. loading={loading}
  687. fallback={fallback}
  688. />
  689. <ConfirmationDialogManager />
  690. </ValidationProvider>
  691. </ActionProvider>
  692. </VisibilityProvider>
  693. </StateProvider>
  694. );
  695. };
  696. }