renderer.tsx 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. "use client";
  2. import React, { type ComponentType, type ReactNode, useMemo } from "react";
  3. import type {
  4. UIElement,
  5. UITree,
  6. Action,
  7. Catalog,
  8. ComponentDefinition,
  9. } from "@json-render/core";
  10. import { useIsVisible } from "./contexts/visibility";
  11. import { useActions } from "./contexts/actions";
  12. import { useData } from "./contexts/data";
  13. /**
  14. * Props passed to component renderers
  15. */
  16. export interface ComponentRenderProps<P = Record<string, unknown>> {
  17. /** The element being rendered */
  18. element: UIElement<string, P>;
  19. /** Rendered children */
  20. children?: ReactNode;
  21. /** Execute an action */
  22. onAction?: (action: Action) => void;
  23. /** Whether the parent is loading */
  24. loading?: boolean;
  25. }
  26. /**
  27. * Component renderer type
  28. */
  29. export type ComponentRenderer<P = Record<string, unknown>> = ComponentType<
  30. ComponentRenderProps<P>
  31. >;
  32. /**
  33. * Registry of component renderers
  34. */
  35. export type ComponentRegistry = Record<string, ComponentRenderer<any>>;
  36. /**
  37. * Props for the Renderer component
  38. */
  39. export interface RendererProps {
  40. /** The UI tree to render */
  41. tree: UITree | null;
  42. /** Component registry */
  43. registry: ComponentRegistry;
  44. /** Whether the tree is currently loading/streaming */
  45. loading?: boolean;
  46. /** Fallback component for unknown types */
  47. fallback?: ComponentRenderer;
  48. }
  49. /**
  50. * Element renderer component
  51. */
  52. function ElementRenderer({
  53. element,
  54. tree,
  55. registry,
  56. loading,
  57. fallback,
  58. }: {
  59. element: UIElement;
  60. tree: UITree;
  61. registry: ComponentRegistry;
  62. loading?: boolean;
  63. fallback?: ComponentRenderer;
  64. }) {
  65. const isVisible = useIsVisible(element.visible);
  66. const { execute } = useActions();
  67. // Don't render if not visible
  68. if (!isVisible) {
  69. return null;
  70. }
  71. // Get the component renderer
  72. const Component = registry[element.type] ?? fallback;
  73. if (!Component) {
  74. console.warn(`No renderer for component type: ${element.type}`);
  75. return null;
  76. }
  77. // Render children
  78. const children = element.children?.map((childKey) => {
  79. const childElement = tree.elements[childKey];
  80. if (!childElement) {
  81. return null;
  82. }
  83. return (
  84. <ElementRenderer
  85. key={childKey}
  86. element={childElement}
  87. tree={tree}
  88. registry={registry}
  89. loading={loading}
  90. fallback={fallback}
  91. />
  92. );
  93. });
  94. return (
  95. <Component element={element} onAction={execute} loading={loading}>
  96. {children}
  97. </Component>
  98. );
  99. }
  100. /**
  101. * Main renderer component
  102. */
  103. export function Renderer({ tree, registry, loading, fallback }: RendererProps) {
  104. if (!tree || !tree.root) {
  105. return null;
  106. }
  107. const rootElement = tree.elements[tree.root];
  108. if (!rootElement) {
  109. return null;
  110. }
  111. return (
  112. <ElementRenderer
  113. element={rootElement}
  114. tree={tree}
  115. registry={registry}
  116. loading={loading}
  117. fallback={fallback}
  118. />
  119. );
  120. }
  121. /**
  122. * Props for JSONUIProvider
  123. */
  124. export interface JSONUIProviderProps {
  125. /** Component registry */
  126. registry: ComponentRegistry;
  127. /** Initial data model */
  128. initialData?: Record<string, unknown>;
  129. /** Auth state */
  130. authState?: { isSignedIn: boolean; user?: Record<string, unknown> };
  131. /** Action handlers */
  132. actionHandlers?: Record<
  133. string,
  134. (params: Record<string, unknown>) => Promise<unknown> | unknown
  135. >;
  136. /** Navigation function */
  137. navigate?: (path: string) => void;
  138. /** Custom validation functions */
  139. validationFunctions?: Record<
  140. string,
  141. (value: unknown, args?: Record<string, unknown>) => boolean
  142. >;
  143. /** Callback when data changes */
  144. onDataChange?: (path: string, value: unknown) => void;
  145. children: ReactNode;
  146. }
  147. // Import the providers
  148. import { DataProvider } from "./contexts/data";
  149. import { VisibilityProvider } from "./contexts/visibility";
  150. import { ActionProvider } from "./contexts/actions";
  151. import { ValidationProvider } from "./contexts/validation";
  152. import { ConfirmDialog } from "./contexts/actions";
  153. /**
  154. * Combined provider for all JSONUI contexts
  155. */
  156. export function JSONUIProvider({
  157. registry,
  158. initialData,
  159. authState,
  160. actionHandlers,
  161. navigate,
  162. validationFunctions,
  163. onDataChange,
  164. children,
  165. }: JSONUIProviderProps) {
  166. return (
  167. <DataProvider
  168. initialData={initialData}
  169. authState={authState}
  170. onDataChange={onDataChange}
  171. >
  172. <VisibilityProvider>
  173. <ActionProvider handlers={actionHandlers} navigate={navigate}>
  174. <ValidationProvider customFunctions={validationFunctions}>
  175. {children}
  176. <ConfirmationDialogManager />
  177. </ValidationProvider>
  178. </ActionProvider>
  179. </VisibilityProvider>
  180. </DataProvider>
  181. );
  182. }
  183. /**
  184. * Renders the confirmation dialog when needed
  185. */
  186. function ConfirmationDialogManager() {
  187. const { pendingConfirmation, confirm, cancel } = useActions();
  188. if (!pendingConfirmation?.action.confirm) {
  189. return null;
  190. }
  191. return (
  192. <ConfirmDialog
  193. confirm={pendingConfirmation.action.confirm}
  194. onConfirm={confirm}
  195. onCancel={cancel}
  196. />
  197. );
  198. }
  199. /**
  200. * Helper to create a renderer component from a catalog
  201. */
  202. export function createRendererFromCatalog<
  203. C extends Catalog<Record<string, ComponentDefinition>>,
  204. >(
  205. _catalog: C,
  206. registry: ComponentRegistry,
  207. ): ComponentType<Omit<RendererProps, "registry">> {
  208. return function CatalogRenderer(props: Omit<RendererProps, "registry">) {
  209. return <Renderer {...props} registry={registry} />;
  210. };
  211. }