mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-09-14 13:41:32 +08:00
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useMemo, type ReactNode } from "react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
Renderer,
|
|
type Spec,
|
|
StateProvider,
|
|
VisibilityProvider,
|
|
ActionProvider,
|
|
ValidationProvider,
|
|
useValidation,
|
|
} from "@json-render/react";
|
|
import { JsonRenderDevtools } from "@json-render/devtools-react";
|
|
import type { Catalog } from "@json-render/core";
|
|
|
|
import { registry, Fallback } from "./registry";
|
|
import { playgroundCatalog } from "./catalog";
|
|
|
|
// =============================================================================
|
|
// PlaygroundRenderer
|
|
// =============================================================================
|
|
|
|
interface PlaygroundRendererProps {
|
|
spec: Spec | null;
|
|
data?: Record<string, unknown>;
|
|
loading?: boolean;
|
|
/** Show the json-render devtools panel. Default: false. */
|
|
devtools?: boolean;
|
|
}
|
|
|
|
const fallbackRenderer = (renderProps: { element: { type: string } }) => (
|
|
<Fallback type={renderProps.element.type} />
|
|
);
|
|
|
|
/**
|
|
* Inner component that sits inside ValidationProvider so it can call
|
|
* useValidation() and wire validateAll into the formSubmit action handler.
|
|
*
|
|
* ActionProvider stores `handlers` in useState, so it only reads the initial
|
|
* value. We use a ref so the handlers object is stable (created once) but
|
|
* formSubmit always reads the latest validateAll.
|
|
*/
|
|
function ValidatedActions({ children }: { children: ReactNode }) {
|
|
const { validateAll } = useValidation();
|
|
const validateAllRef = useRef(validateAll);
|
|
validateAllRef.current = validateAll;
|
|
|
|
const handlers = useMemo<
|
|
Record<string, (params: Record<string, unknown>) => void>
|
|
>(
|
|
() => ({
|
|
buttonClick: (params) => {
|
|
const message = (params?.message as string) || "Button clicked!";
|
|
toast.success(message);
|
|
},
|
|
formSubmit: () => {
|
|
const allValid = validateAllRef.current();
|
|
if (!allValid) {
|
|
toast.error("Please fix the errors before submitting.");
|
|
return;
|
|
}
|
|
toast.success("Form submitted successfully!");
|
|
},
|
|
linkClick: (params) => {
|
|
const href = (params?.href as string) || "#";
|
|
toast.info(`Navigating to: ${href}`);
|
|
},
|
|
}),
|
|
[], // stable — ref ensures latest validateAll is always used
|
|
);
|
|
|
|
return <ActionProvider handlers={handlers}>{children}</ActionProvider>;
|
|
}
|
|
|
|
export function PlaygroundRenderer({
|
|
spec,
|
|
data,
|
|
loading,
|
|
devtools,
|
|
}: PlaygroundRendererProps): ReactNode {
|
|
if (!spec) return null;
|
|
|
|
return (
|
|
<StateProvider initialState={data ?? spec.state}>
|
|
<VisibilityProvider>
|
|
<ValidationProvider>
|
|
<ValidatedActions>
|
|
<Renderer
|
|
spec={spec}
|
|
registry={registry}
|
|
fallback={fallbackRenderer}
|
|
loading={loading}
|
|
/>
|
|
{devtools ? (
|
|
<JsonRenderDevtools
|
|
spec={spec}
|
|
catalog={playgroundCatalog as unknown as Catalog}
|
|
/>
|
|
) : null}
|
|
</ValidatedActions>
|
|
</ValidationProvider>
|
|
</VisibilityProvider>
|
|
</StateProvider>
|
|
);
|
|
}
|