page-renderer.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. "use client";
  2. import React, { useMemo, type ReactNode } from "react";
  3. import type { Spec } from "@json-render/core";
  4. import {
  5. Renderer,
  6. StateProvider,
  7. VisibilityProvider,
  8. ValidationProvider,
  9. ActionProvider,
  10. type ComponentRegistry,
  11. type ComponentRenderProps,
  12. } from "@json-render/react";
  13. import { useNextApp } from "./provider";
  14. /**
  15. * Props for PageRenderer.
  16. */
  17. export interface PageRendererProps {
  18. /** Page element tree spec */
  19. spec: Spec;
  20. /** Initial state (merged from spec.state, global state, and loader data) */
  21. initialState?: Record<string, unknown>;
  22. /** Optional layout element tree spec */
  23. layoutSpec?: Spec | null;
  24. /** Whether the spec is currently loading/streaming */
  25. loading?: boolean;
  26. }
  27. /**
  28. * Built-in Slot component that renders children passed into layouts.
  29. */
  30. function SlotComponent({ children }: ComponentRenderProps) {
  31. return <>{children}</>;
  32. }
  33. /**
  34. * Client component that renders a json-render page within the Next.js app.
  35. *
  36. * Reads the component registry and action handlers from NextAppProvider context.
  37. * Handles layout rendering by injecting page content into a Slot component.
  38. */
  39. export function PageRenderer({
  40. spec,
  41. initialState,
  42. layoutSpec,
  43. loading,
  44. }: PageRendererProps) {
  45. const { registry, handlers, navigate } = useNextApp();
  46. const augmentedRegistry: ComponentRegistry = useMemo(() => {
  47. return {
  48. ...registry,
  49. Slot: SlotComponent,
  50. };
  51. }, [registry]);
  52. const actionHandlers = useMemo(() => {
  53. const base: Record<
  54. string,
  55. (params: Record<string, unknown>) => Promise<unknown> | unknown
  56. > = { ...handlers };
  57. base.navigate = (params: Record<string, unknown>) => {
  58. const href = params.href as string | undefined;
  59. if (href) navigate(href);
  60. };
  61. return base;
  62. }, [handlers, navigate]);
  63. const pageContent = (
  64. <Renderer spec={spec} registry={augmentedRegistry} loading={loading} />
  65. );
  66. if (layoutSpec) {
  67. return (
  68. <StateProvider initialState={initialState}>
  69. <VisibilityProvider>
  70. <ValidationProvider>
  71. <ActionProvider handlers={actionHandlers} navigate={navigate}>
  72. <LayoutWithSlot
  73. layoutSpec={layoutSpec}
  74. registry={augmentedRegistry}
  75. loading={loading}
  76. >
  77. {pageContent}
  78. </LayoutWithSlot>
  79. </ActionProvider>
  80. </ValidationProvider>
  81. </VisibilityProvider>
  82. </StateProvider>
  83. );
  84. }
  85. return (
  86. <StateProvider initialState={initialState}>
  87. <VisibilityProvider>
  88. <ValidationProvider>
  89. <ActionProvider handlers={actionHandlers} navigate={navigate}>
  90. {pageContent}
  91. </ActionProvider>
  92. </ValidationProvider>
  93. </VisibilityProvider>
  94. </StateProvider>
  95. );
  96. }
  97. /**
  98. * Renders a layout spec and injects children into the Slot.
  99. */
  100. function LayoutWithSlot({
  101. layoutSpec,
  102. registry,
  103. loading,
  104. children,
  105. }: {
  106. layoutSpec: Spec;
  107. registry: ComponentRegistry;
  108. loading?: boolean;
  109. children: ReactNode;
  110. }) {
  111. const layoutRegistry: ComponentRegistry = useMemo(() => {
  112. return {
  113. ...registry,
  114. Slot: function LayoutSlot() {
  115. return <>{children}</>;
  116. },
  117. };
  118. }, [registry, children]);
  119. return (
  120. <Renderer spec={layoutSpec} registry={layoutRegistry} loading={loading} />
  121. );
  122. }