renderer.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  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. StateStore,
  15. } from "@json-render/core";
  16. import {
  17. resolveElementProps,
  18. resolveBindings,
  19. resolveActionParam,
  20. evaluateVisibility,
  21. getByPath,
  22. type PropResolutionContext,
  23. type VisibilityContext as CoreVisibilityContext,
  24. } from "@json-render/core";
  25. import type { Components, SetState, StateModel } from "./catalog-types";
  26. import { useIsVisible, useVisibility } from "./contexts/visibility";
  27. import { useActions } from "./contexts/actions";
  28. import { useStateStore } from "./contexts/state";
  29. import { StateProvider } from "./contexts/state";
  30. import { VisibilityProvider } from "./contexts/visibility";
  31. import { ActionProvider } from "./contexts/actions";
  32. import { ValidationProvider } from "./contexts/validation";
  33. import { standardComponents } from "./components/standard";
  34. import { RepeatScopeProvider, useRepeatScope } from "./contexts/repeat-scope";
  35. // =============================================================================
  36. // Types
  37. // =============================================================================
  38. export interface ComponentRenderProps<P = Record<string, unknown>> {
  39. element: UIElement<string, P>;
  40. children?: ReactNode;
  41. emit: (event: string) => void;
  42. bindings?: Record<string, string>;
  43. loading?: boolean;
  44. }
  45. export type ComponentRenderer<P = Record<string, unknown>> = ComponentType<
  46. ComponentRenderProps<P>
  47. >;
  48. export type ComponentRegistry = Record<string, ComponentRenderer<any>>;
  49. export interface RendererProps {
  50. spec: Spec | null;
  51. registry?: ComponentRegistry;
  52. includeStandard?: boolean;
  53. loading?: boolean;
  54. fallback?: ComponentRenderer;
  55. }
  56. // =============================================================================
  57. // ElementErrorBoundary
  58. // =============================================================================
  59. interface ElementErrorBoundaryProps {
  60. elementType: string;
  61. children: ReactNode;
  62. }
  63. interface ElementErrorBoundaryState {
  64. hasError: boolean;
  65. }
  66. class ElementErrorBoundary extends React.Component<
  67. ElementErrorBoundaryProps,
  68. ElementErrorBoundaryState
  69. > {
  70. constructor(props: ElementErrorBoundaryProps) {
  71. super(props);
  72. this.state = { hasError: false };
  73. }
  74. static getDerivedStateFromError(): ElementErrorBoundaryState {
  75. return { hasError: true };
  76. }
  77. componentDidCatch(error: Error, info: ErrorInfo) {
  78. console.error(
  79. `[json-render/react-pdf] Rendering error in <${this.props.elementType}>:`,
  80. error,
  81. info.componentStack,
  82. );
  83. }
  84. render() {
  85. if (this.state.hasError) {
  86. return null;
  87. }
  88. return this.props.children;
  89. }
  90. }
  91. // =============================================================================
  92. // ElementRenderer
  93. // =============================================================================
  94. interface ElementRendererProps {
  95. element: UIElement;
  96. spec: Spec;
  97. registry: ComponentRegistry;
  98. loading?: boolean;
  99. fallback?: ComponentRenderer;
  100. }
  101. const ElementRenderer = React.memo(function ElementRenderer({
  102. element,
  103. spec,
  104. registry,
  105. loading,
  106. fallback,
  107. }: ElementRendererProps) {
  108. const repeatScope = useRepeatScope();
  109. const { ctx } = useVisibility();
  110. const { execute } = useActions();
  111. const { getSnapshot } = useStateStore();
  112. const fullCtx: PropResolutionContext = useMemo(
  113. () =>
  114. repeatScope
  115. ? {
  116. ...ctx,
  117. repeatItem: repeatScope.item,
  118. repeatIndex: repeatScope.index,
  119. repeatBasePath: repeatScope.basePath,
  120. }
  121. : ctx,
  122. [ctx, repeatScope],
  123. );
  124. const isVisible =
  125. element.visible === undefined
  126. ? true
  127. : evaluateVisibility(element.visible, fullCtx);
  128. const onBindings = element.on;
  129. const emit = useCallback(
  130. async (eventName: string) => {
  131. const binding = onBindings?.[eventName];
  132. if (!binding) return;
  133. const actionBindings = Array.isArray(binding) ? binding : [binding];
  134. for (const b of actionBindings) {
  135. if (!b.params) {
  136. await execute(b);
  137. continue;
  138. }
  139. // Build a fresh context with live store state so that $state
  140. // references in later actions see mutations from earlier ones.
  141. const liveCtx: PropResolutionContext = {
  142. ...fullCtx,
  143. stateModel: getSnapshot(),
  144. };
  145. const resolved: Record<string, unknown> = {};
  146. for (const [key, val] of Object.entries(b.params)) {
  147. resolved[key] = resolveActionParam(val, liveCtx);
  148. }
  149. await execute({ ...b, params: resolved });
  150. }
  151. },
  152. [onBindings, execute, fullCtx, getSnapshot],
  153. );
  154. if (!isVisible) {
  155. return null;
  156. }
  157. const rawProps = element.props as Record<string, unknown>;
  158. const elementBindings = resolveBindings(rawProps, fullCtx);
  159. const resolvedProps = resolveElementProps(rawProps, fullCtx);
  160. const resolvedElement =
  161. resolvedProps !== element.props
  162. ? { ...element, props: resolvedProps }
  163. : element;
  164. const Component = registry[resolvedElement.type] ?? fallback;
  165. if (!Component) {
  166. console.warn(
  167. `[json-render/react-pdf] No renderer for component type: ${resolvedElement.type}`,
  168. );
  169. return null;
  170. }
  171. const children = resolvedElement.repeat ? (
  172. <RepeatChildren
  173. element={resolvedElement}
  174. spec={spec}
  175. registry={registry}
  176. loading={loading}
  177. fallback={fallback}
  178. />
  179. ) : (
  180. resolvedElement.children?.map((childKey) => {
  181. const childElement = spec.elements[childKey];
  182. if (!childElement) {
  183. if (!loading) {
  184. console.warn(
  185. `[json-render/react-pdf] Missing element "${childKey}" referenced as child of "${resolvedElement.type}".`,
  186. );
  187. }
  188. return null;
  189. }
  190. return (
  191. <ElementRenderer
  192. key={childKey}
  193. element={childElement}
  194. spec={spec}
  195. registry={registry}
  196. loading={loading}
  197. fallback={fallback}
  198. />
  199. );
  200. })
  201. );
  202. return (
  203. <ElementErrorBoundary elementType={resolvedElement.type}>
  204. <Component
  205. element={resolvedElement}
  206. emit={emit}
  207. bindings={elementBindings}
  208. loading={loading}
  209. >
  210. {children}
  211. </Component>
  212. </ElementErrorBoundary>
  213. );
  214. });
  215. // =============================================================================
  216. // RepeatChildren
  217. // =============================================================================
  218. function RepeatChildren({
  219. element,
  220. spec,
  221. registry,
  222. loading,
  223. fallback,
  224. }: {
  225. element: UIElement;
  226. spec: Spec;
  227. registry: ComponentRegistry;
  228. loading?: boolean;
  229. fallback?: ComponentRenderer;
  230. }) {
  231. const { state } = useStateStore();
  232. const repeat = element.repeat!;
  233. const statePath = repeat.statePath;
  234. const items = (getByPath(state, statePath) as unknown[] | undefined) ?? [];
  235. return (
  236. <>
  237. {items.map((itemValue, index) => {
  238. const key =
  239. repeat.key && typeof itemValue === "object" && itemValue !== null
  240. ? String(
  241. (itemValue as Record<string, unknown>)[repeat.key] ?? index,
  242. )
  243. : String(index);
  244. return (
  245. <RepeatScopeProvider
  246. key={key}
  247. item={itemValue}
  248. index={index}
  249. basePath={`${statePath}/${index}`}
  250. >
  251. {element.children?.map((childKey) => {
  252. const childElement = spec.elements[childKey];
  253. if (!childElement) {
  254. if (!loading) {
  255. console.warn(
  256. `[json-render/react-pdf] Missing element "${childKey}" referenced as child of "${element.type}" (repeat).`,
  257. );
  258. }
  259. return null;
  260. }
  261. return (
  262. <ElementRenderer
  263. key={childKey}
  264. element={childElement}
  265. spec={spec}
  266. registry={registry}
  267. loading={loading}
  268. fallback={fallback}
  269. />
  270. );
  271. })}
  272. </RepeatScopeProvider>
  273. );
  274. })}
  275. </>
  276. );
  277. }
  278. // =============================================================================
  279. // Renderer
  280. // =============================================================================
  281. export function Renderer({
  282. spec,
  283. registry: customRegistry,
  284. includeStandard = true,
  285. loading,
  286. fallback,
  287. }: RendererProps) {
  288. const registry: ComponentRegistry = useMemo(
  289. () => ({
  290. ...(includeStandard ? standardComponents : {}),
  291. ...customRegistry,
  292. }),
  293. [customRegistry, includeStandard],
  294. );
  295. if (!spec || !spec.root) {
  296. return null;
  297. }
  298. const rootElement = spec.elements[spec.root];
  299. if (!rootElement) {
  300. return null;
  301. }
  302. return (
  303. <ElementRenderer
  304. element={rootElement}
  305. spec={spec}
  306. registry={registry}
  307. loading={loading}
  308. fallback={fallback}
  309. />
  310. );
  311. }
  312. // =============================================================================
  313. // JSONUIProvider
  314. // =============================================================================
  315. export interface JSONUIProviderProps {
  316. registry?: ComponentRegistry;
  317. store?: StateStore;
  318. initialState?: Record<string, unknown>;
  319. handlers?: Record<
  320. string,
  321. (params: Record<string, unknown>) => Promise<unknown> | unknown
  322. >;
  323. navigate?: (path: string) => void;
  324. validationFunctions?: Record<
  325. string,
  326. (value: unknown, args?: Record<string, unknown>) => boolean
  327. >;
  328. onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
  329. children: ReactNode;
  330. }
  331. export function JSONUIProvider({
  332. store,
  333. initialState,
  334. handlers,
  335. navigate,
  336. validationFunctions,
  337. onStateChange,
  338. children,
  339. }: JSONUIProviderProps) {
  340. return (
  341. <StateProvider
  342. store={store}
  343. initialState={initialState}
  344. onStateChange={onStateChange}
  345. >
  346. <VisibilityProvider>
  347. <ActionProvider handlers={handlers} navigate={navigate}>
  348. <ValidationProvider customFunctions={validationFunctions}>
  349. {children}
  350. </ValidationProvider>
  351. </ActionProvider>
  352. </VisibilityProvider>
  353. </StateProvider>
  354. );
  355. }
  356. // =============================================================================
  357. // defineRegistry
  358. // =============================================================================
  359. export interface DefineRegistryResult {
  360. registry: ComponentRegistry;
  361. }
  362. type DefineRegistryComponentFn = (ctx: {
  363. props: unknown;
  364. children?: React.ReactNode;
  365. emit: (event: string) => void;
  366. bindings?: Record<string, string>;
  367. loading?: boolean;
  368. }) => React.ReactNode;
  369. export function defineRegistry<C extends Catalog>(
  370. _catalog: C,
  371. options: {
  372. components?: Components<C>;
  373. },
  374. ): DefineRegistryResult {
  375. const registry: ComponentRegistry = {};
  376. if (options.components) {
  377. for (const [name, componentFn] of Object.entries(options.components)) {
  378. registry[name] = ({
  379. element,
  380. children,
  381. emit,
  382. bindings,
  383. loading,
  384. }: ComponentRenderProps) => {
  385. return (componentFn as DefineRegistryComponentFn)({
  386. props: element.props,
  387. children,
  388. emit,
  389. bindings,
  390. loading,
  391. });
  392. };
  393. }
  394. }
  395. return { registry };
  396. }
  397. // =============================================================================
  398. // createRenderer
  399. // =============================================================================
  400. export interface CreateRendererProps {
  401. spec: Spec | null;
  402. store?: StateStore;
  403. state?: Record<string, unknown>;
  404. onAction?: (actionName: string, params?: Record<string, unknown>) => void;
  405. onStateChange?: (changes: Array<{ path: string; value: unknown }>) => void;
  406. loading?: boolean;
  407. fallback?: ComponentRenderer;
  408. }
  409. export type ComponentMap<
  410. TComponents extends Record<string, { props: unknown }>,
  411. > = {
  412. [K in keyof TComponents]: ComponentType<
  413. ComponentRenderProps<
  414. TComponents[K]["props"] extends { _output: infer O }
  415. ? O
  416. : Record<string, unknown>
  417. >
  418. >;
  419. };
  420. export function createRenderer<
  421. TDef extends SchemaDefinition,
  422. TCatalog extends { components: Record<string, { props: unknown }> },
  423. >(
  424. catalog: Catalog<TDef, TCatalog>,
  425. components: ComponentMap<TCatalog["components"]>,
  426. ): ComponentType<CreateRendererProps> {
  427. const registry: ComponentRegistry =
  428. components as unknown as ComponentRegistry;
  429. return function CatalogRenderer({
  430. spec,
  431. store,
  432. state,
  433. onAction,
  434. onStateChange,
  435. loading,
  436. fallback,
  437. }: CreateRendererProps) {
  438. const actionHandlers = onAction
  439. ? new Proxy(
  440. {} as Record<
  441. string,
  442. (params: Record<string, unknown>) => void | Promise<void>
  443. >,
  444. {
  445. get: (_target, prop: string) => {
  446. return (params: Record<string, unknown>) =>
  447. onAction(prop, params);
  448. },
  449. has: () => true,
  450. },
  451. )
  452. : undefined;
  453. return (
  454. <StateProvider
  455. store={store}
  456. initialState={state}
  457. onStateChange={onStateChange}
  458. >
  459. <VisibilityProvider>
  460. <ActionProvider handlers={actionHandlers}>
  461. <ValidationProvider>
  462. <Renderer
  463. spec={spec}
  464. registry={registry}
  465. loading={loading}
  466. fallback={fallback}
  467. />
  468. </ValidationProvider>
  469. </ActionProvider>
  470. </VisibilityProvider>
  471. </StateProvider>
  472. );
  473. };
  474. }