@novahelm/forms
v2026.6.1
Published
NovaHelm forms — defineForm() schema-driven forms and FormRenderer.
Maintainers
Readme
@novahelm/forms
Declarative, type-safe form system for NovaHelm applications.
Define forms once in TypeScript, validate on the server with auto-generated Zod schemas, and render them anywhere with the built-in React renderer.
Quick Start
pnpm add @novahelm/formsimport { defineForm, FormRenderer } from "@novahelm/forms";
const contactForm = defineForm({
slug: "contact",
title: "Contact Us",
fields: [
{ name: "name", type: "text", required: true },
{ name: "email", type: "email", required: true },
{ name: "message", type: "richtext", multiline: true },
],
onSubmit: "api",
submitEndpoint: "/api/contact",
});
// Render in any React component
export function ContactPage() {
return (
<FormRenderer
form={contactForm}
onSubmit={async (values) => {
await fetch("/api/contact", { method: "POST", body: JSON.stringify(values) });
}}
/>
);
}Field Types
| Category | Types |
|----------|-------|
| Text | text, richtext, slug, email, url, color, phone |
| Numeric | number, integer, currency, rating |
| Boolean | boolean |
| Choice | select, multiselect, tags |
| Date/Time | date, dateonly, time |
| Relation | relation |
| Media | image, file, video, gallery |
| Structured | json, array |
| AI | vector |
Server-Side Validation
buildFormSchema() generates a Zod schema from your form config. Use it in tRPC procedures, API routes, or any server-side validation:
import { buildFormSchema, buildFormDefaults } from "@novahelm/forms";
// In a tRPC procedure
const schema = buildFormSchema(contactForm);
export const contactRouter = router({
submit: publicProcedure
.input(schema)
.mutation(async ({ input }) => {
// input is fully typed based on your form fields
await sendEmail(input.email, input.message);
}),
});Conditional Visibility
Show or hide fields based on other fields' values using visibleWhen:
defineForm({
slug: "feedback",
title: "Feedback",
fields: [
{
name: "satisfied",
type: "boolean",
label: "Are you satisfied?",
},
{
name: "reason",
type: "text",
label: "Why not?",
visibleWhen: { field: "satisfied", op: "eq", value: false },
},
],
});Supported operators: eq (default), neq, in, gt, lt
Sections
Group fields into collapsible sections:
defineForm({
slug: "profile",
title: "Edit Profile",
sections: [
{ id: "personal", label: "Personal Info" },
{ id: "contact", label: "Contact Details", collapsible: true },
],
fields: [
{ name: "name", type: "text", section: "personal" },
{ name: "bio", type: "richtext", section: "personal", multiline: true },
{ name: "email", type: "email", section: "contact" },
{ name: "phone", type: "phone", section: "contact" },
],
});Collection Adapter
Auto-generate a form from an admin-kit collection — no duplication:
import { collectionToForm } from "@novahelm/forms";
import { postsCollection } from "@/collections/posts";
// "create" mode: title = "Create Post", submit label = "Create"
const createForm = collectionToForm(postsCollection, "create");
// "edit" mode (default): title = "Post", submit label = "Save"
const editForm = collectionToForm(postsCollection, "edit");Fields with showInForm: false or admin.formHidden: true are automatically excluded.
Extending Forms
Extend an existing form — useful for multi-step flows, locale variants, or restricted versions:
import { extendForm } from "@novahelm/forms";
const shortContactForm = extendForm(contactForm, {
title: "Quick Contact",
fields: contactForm.fields.filter(f => f.required),
});Form Registry
Register forms centrally and look them up at runtime (useful for dynamic form rendering):
import { registerForm, getForm } from "@novahelm/forms";
registerForm(contactForm);
// Later, in a route handler:
const form = getForm("contact");
if (!form) throw new Error("Form not found");API Reference
| Export | Description |
|--------|-------------|
| defineForm(config) | Create a form definition with defaults applied |
| extendForm(base, overrides) | Extend an existing form |
| registerForm(form) | Add a form to the global registry |
| getForm(slug) | Look up a form by slug |
| getForms() | Return all registered forms |
| buildFormSchema(form) | Generate a Zod validation schema |
| buildFormDefaults(form) | Generate initial form values |
| collectionToForm(collection, mode?) | Convert admin-kit collection to form |
| <FormRenderer form onSubmit /> | Render a complete form |
| <FieldRenderer field value onChange /> | Render a single field |
| evaluateCondition(condition, values) | Evaluate a visibleWhen rule |
