Documentation Index
Fetch the complete documentation index at: https://docs-terra.withunify.org/llms.txt
Use this file to discover all available pages before exploring further.
Form Builder Internals
The form builder uses Zustand for state management and a registry pattern for field rendering.
Architecture Overview
Zustand Store
// src/stores/form-builder-store.ts
interface FormBuilderState {
// Schema state
schema: FormSchema;
selectedFieldId: string | null;
selectedPageId: string | null;
// Dirty tracking
isDirty: boolean;
lastSavedAt: Date | null;
// Actions
setSchema: (schema: FormSchema) => void;
addField: (pageId: string, field: FormElement) => void;
updateField: (fieldId: string, updates: Partial<FormElement>) => void;
removeField: (fieldId: string) => void;
moveField: (fieldId: string, newIndex: number) => void;
selectField: (fieldId: string | null) => void;
}
export const useFormBuilder = create<FormBuilderState>((set, get) => ({
schema: initialSchema,
selectedFieldId: null,
isDirty: false,
addField: (pageId, field) => {
set((state) => ({
schema: addFieldToPage(state.schema, pageId, field),
isDirty: true,
}));
},
updateField: (fieldId, updates) => {
set((state) => ({
schema: updateFieldInSchema(state.schema, fieldId, updates),
isDirty: true,
}));
},
// ...
}));
Field Registry
Instead of a giant switch statement, we use a registry:
// src/components/engine/registry.tsx
import { TextFieldRenderer } from "./fields/text-field";
import { ChoiceFieldRenderer } from "./fields/choice-field";
import { AddressFieldRenderer } from "./fields/address-field";
// ...
export const fieldRenderers: Record<string, React.ComponentType<FieldProps>> = {
text: TextFieldRenderer,
number: NumberFieldRenderer,
date: DateFieldRenderer,
choice: ChoiceFieldRenderer,
address: AddressFieldRenderer,
files: FilesFieldRenderer,
group: GroupFieldRenderer,
repeated: RepeatedFieldRenderer,
// ... all field types
};
export function renderField(element: FormElement, props: FieldProps) {
const Renderer = fieldRenderers[element.type];
if (!Renderer) {
console.warn(`Unknown field type: ${element.type}`);
return null;
}
return <Renderer element={element} {...props} />;
}
Adding a New Field Type
- Define the schema in
src/types/schema.ts
- Create the renderer in
src/components/engine/fields/
- Add to registry in
src/components/engine/registry.tsx
- Add toolbox entry in
src/components/form-builder/toolbox.tsx
- Add properties panel in
src/components/form-builder/properties-panel.tsx
Example: Rating field
// 1. Schema
export const RatingFieldSchema = BaseElementSchema.extend({
type: z.literal("rating"),
maxStars: z.number().default(5),
});
// 2. Renderer
export function RatingFieldRenderer({ element, value, onChange }) {
return (
<div className="flex gap-1">
{Array.from({ length: element.maxStars }).map((_, i) => (
<button key={i} onClick={() => onChange(i + 1)}>
{i < value ? "★" : "☆"}
</button>
))}
</div>
);
}
// 3. Registry
fieldRenderers.rating = RatingFieldRenderer;
Schema Design
Form schema structure
Logic Engine
Conditional visibility