renderer.tsx 5.3 KB

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