renderer.tsx 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. "use client";
  2. import { useRef, useMemo, type ReactNode } from "react";
  3. import { toast } from "sonner";
  4. import {
  5. Renderer,
  6. type Spec,
  7. StateProvider,
  8. VisibilityProvider,
  9. ActionProvider,
  10. ValidationProvider,
  11. useValidation,
  12. } from "@json-render/react";
  13. import { JsonRenderDevtools } from "@json-render/devtools-react";
  14. import type { Catalog } from "@json-render/core";
  15. import { registry, Fallback } from "./registry";
  16. import { playgroundCatalog } from "./catalog";
  17. // =============================================================================
  18. // PlaygroundRenderer
  19. // =============================================================================
  20. interface PlaygroundRendererProps {
  21. spec: Spec | null;
  22. data?: Record<string, unknown>;
  23. loading?: boolean;
  24. /** Show the json-render devtools panel. Default: false. */
  25. devtools?: boolean;
  26. }
  27. const fallbackRenderer = (renderProps: { element: { type: string } }) => (
  28. <Fallback type={renderProps.element.type} />
  29. );
  30. /**
  31. * Inner component that sits inside ValidationProvider so it can call
  32. * useValidation() and wire validateAll into the formSubmit action handler.
  33. *
  34. * ActionProvider stores `handlers` in useState, so it only reads the initial
  35. * value. We use a ref so the handlers object is stable (created once) but
  36. * formSubmit always reads the latest validateAll.
  37. */
  38. function ValidatedActions({ children }: { children: ReactNode }) {
  39. const { validateAll } = useValidation();
  40. const validateAllRef = useRef(validateAll);
  41. validateAllRef.current = validateAll;
  42. const handlers = useMemo<
  43. Record<string, (params: Record<string, unknown>) => void>
  44. >(
  45. () => ({
  46. buttonClick: (params) => {
  47. const message = (params?.message as string) || "Button clicked!";
  48. toast.success(message);
  49. },
  50. formSubmit: () => {
  51. const allValid = validateAllRef.current();
  52. if (!allValid) {
  53. toast.error("Please fix the errors before submitting.");
  54. return;
  55. }
  56. toast.success("Form submitted successfully!");
  57. },
  58. linkClick: (params) => {
  59. const href = (params?.href as string) || "#";
  60. toast.info(`Navigating to: ${href}`);
  61. },
  62. }),
  63. [], // stable — ref ensures latest validateAll is always used
  64. );
  65. return <ActionProvider handlers={handlers}>{children}</ActionProvider>;
  66. }
  67. export function PlaygroundRenderer({
  68. spec,
  69. data,
  70. loading,
  71. devtools,
  72. }: PlaygroundRendererProps): ReactNode {
  73. if (!spec) return null;
  74. return (
  75. <StateProvider initialState={data ?? spec.state}>
  76. <VisibilityProvider>
  77. <ValidationProvider>
  78. <ValidatedActions>
  79. <Renderer
  80. spec={spec}
  81. registry={registry}
  82. fallback={fallbackRenderer}
  83. loading={loading}
  84. />
  85. {devtools ? (
  86. <JsonRenderDevtools
  87. spec={spec}
  88. catalog={playgroundCatalog as unknown as Catalog}
  89. />
  90. ) : null}
  91. </ValidatedActions>
  92. </ValidationProvider>
  93. </VisibilityProvider>
  94. </StateProvider>
  95. );
  96. }