The Function Registry enables binding event handlers to component props from the UI Builder. This allows users to connect component events like onClick, onSubmit, and onChange to pre-defined functions provided by developers.
This feature enables:
Create a function registry with your available functions:
tsx1import { z } from 'zod'; 2import { toast } from 'sonner'; 3import type { FunctionRegistry } from '@/components/ui/ui-builder/types'; 4 5const functionRegistry: FunctionRegistry = { 6 showSuccessToast: { 7 name: "Show Success Toast", 8 schema: z.tuple([]), // No parameters 9 fn: () => { 10 toast.success("Action completed!"); 11 }, 12 description: "Shows a success notification" 13 }, 14 handleFormSubmit: { 15 name: "Submit Form", 16 schema: z.tuple([z.custom<React.FormEvent>()]), 17 fn: async (e: React.FormEvent<HTMLFormElement>) => { 18 e.preventDefault(); 19 const formData = new FormData(e.currentTarget); 20 // Handle form submission 21 await submitToServer(formData); 22 toast.success("Form submitted!"); 23 }, 24 description: "Submits form data to server" 25 }, 26 logClick: { 27 name: "Log Click", 28 schema: z.tuple([z.custom<React.MouseEvent>()]), 29 fn: (e: React.MouseEvent) => { 30 console.log("Button clicked at:", e.clientX, e.clientY); 31 }, 32 description: "Logs click coordinates to console" 33 } 34};
Each function in the registry follows this interface:
tsx1interface FunctionDefinition { 2 /** Human-readable name displayed in the UI */ 3 name: string; 4 5 /** Zod schema describing function parameters */ 6 schema: ZodTuple | ZodObject; 7 8 /** The actual function to call at runtime */ 9 fn: (...args: any[]) => any; 10 11 /** Optional description shown in the UI */ 12 description?: string; 13}
Pass the function registry to the UIBuilder:
tsx1import UIBuilder from '@/components/ui/ui-builder'; 2 3function App() { 4 return ( 5 <UIBuilder 6 componentRegistry={myComponentRegistry} 7 functionRegistry={functionRegistry} 8 /> 9 ); 10}
Function bindings work through the variable system. To bind a function:
The variable now references a function from your registry.
Once you have a function-type variable, bind it to component event props:
The function will now be called when the event fires.
When you export code, function bindings generate a clean interface:
tsx1import React from "react"; 2import { Button } from "@/components/ui/button"; 3 4interface PageProps { 5 variables: { 6 userName: string; 7 }; 8 functions: { 9 handleClick: (...args: any[]) => any; 10 handleSubmit: (...args: any[]) => any; 11 }; 12} 13 14const Page = ({ variables, functions }: PageProps) => { 15 return ( 16 <div> 17 <span>{variables.userName}</span> 18 <Button onClick={functions.handleClick}>Click Me</Button> 19 <form onSubmit={functions.handleSubmit}> 20 {/* form content */} 21 </form> 22 </div> 23 ); 24}; 25 26export default Page;
When rendering pages in read-only mode, provide the function registry:
tsx1import LayerRenderer from '@/components/ui/ui-builder/layer-renderer'; 2 3function ReadOnlyPage() { 4 return ( 5 <LayerRenderer 6 page={savedPage} 7 componentRegistry={componentRegistry} 8 variables={[...savedVariables]} 9 functionRegistry={functionRegistry} 10 /> 11 ); 12}
tsx1import { submitFormAction } from './actions'; 2 3const functionRegistry: FunctionRegistry = { 4 handleContactForm: { 5 name: "Submit Contact Form", 6 schema: z.tuple([z.custom<React.FormEvent>()]), 7 fn: async (e) => { 8 e.preventDefault(); 9 const formData = new FormData(e.currentTarget); 10 const result = await submitFormAction(formData); 11 if (result.success) { 12 toast.success(result.message); 13 } else { 14 toast.error(result.message); 15 } 16 }, 17 description: "Submits contact form to server" 18 } 19};
tsx1const functionRegistry: FunctionRegistry = { 2 showSuccessToast: { 3 name: "Show Success", 4 schema: z.tuple([]), 5 fn: () => toast.success("Success!"), 6 description: "Shows success toast" 7 }, 8 showErrorToast: { 9 name: "Show Error", 10 schema: z.tuple([]), 11 fn: () => toast.error("Error!"), 12 description: "Shows error toast" 13 } 14};
tsx1const functionRegistry: FunctionRegistry = { 2 trackButtonClick: { 3 name: "Track Button Click", 4 schema: z.tuple([z.custom<React.MouseEvent>()]), 5 fn: (e) => { 6 analytics.track('button_clicked', { 7 elementId: e.currentTarget.id, 8 timestamp: Date.now() 9 }); 10 }, 11 description: "Tracks button click in analytics" 12 } 13};
When a function is bound to a prop, the layer stores metadata using the __function_ prefix:
tsx1// Layer JSON representation 2{ 3 type: "Button", 4 props: { 5 __function_onClick: "handleClick" // References function ID 6 } 7}
At render time, the resolveVariableReferences function looks up handleClick in the provided function registry and replaces __function_onClick with the actual function on the onClick prop.
Important: If a layer has __function_* metadata but no functionRegistry is provided to the renderer, the system will:
This allows for graceful degradation but should be avoided in production. Always provide a functionRegistry when rendering pages that contain function bindings.
tsx1// ✅ Correct - provides functionRegistry 2<LayerRenderer 3 page={page} 4 componentRegistry={componentRegistry} 5 functionRegistry={functionRegistry} // Required for function bindings 6/> 7 8// ⚠️ Warning - missing functionRegistry with function bindings 9<LayerRenderer 10 page={pageWithFunctionBindings} 11 componentRegistry={componentRegistry} 12 // functionRegistry omitted - will log warnings 13/>
The function binding system supports flexible signature matching:
(a) can be used where (a, b, c) is expected({name}) can be used where ({name, email, phone}) is expectedThis allows reusing simpler functions across multiple contexts.
📚 Related: See Variables for the variable system overview and Variable Binding for general binding mechanics.