renderer.tsx 2.6 KB

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