@vulcantech/forms
v0.1.1
Published
Dynamic form renderer with TipTap-style builder, Yup validation, sensitive field encryption, and Tailwind styling
Maintainers
Readme
@vulcantech/forms
Dynamic form renderer for VulcanTech apps. Renders a form from a JSON FormDefinition (authored in the CMS form builder) using Formik + Yup, with sensitive-field encryption and Tailwind styling.
Exports: DynamicForm, DynamicFormSection, FormDefinition (type), buildYupSchema, buildInitialValues, plus transform / crypto / email-template helpers.
Field types: text, email, phone, textarea, select, checkbox, radio, number, date, datetime, url, address, hidden.
Integration
Wire forms into a consumer site (Next.js App Router):
1. Add the dependency:
npm install @vulcantech/forms(Inside the VulcanTech monorepo, use the workspace wiring in MONOREPO.md instead.)
2. Set the env — see Required env below.
3. Tailwind content (required) — add the package to Tailwind's sources, or the renderer's classes won't generate and forms render unstyled. Tailwind v4 (in the app's globals.css / tailwind.css); adjust the path to where the package resolves from:
@source "../node_modules/@vulcantech/forms/dist";(JS config: add the equivalent glob to content. Monorepo apps point at the package source instead — see MONOREPO.md.)
4. Server fetcher src/lib/cms/forms.ts — ISR-cached wrapper over the CMS /api/forms. Exposes getFormById(id) / getFormBySlug(slug); tags each fetch form:{projectId}:{formId} + forms:{projectId}; returns null on failure so the page still renders.
5. Per-form loader src/lib/cms/load<Name>Form.ts — pin the form's Firestore id and wrap the fetcher with a fallback:
import { getFormById } from "./forms";
export const CONTACT_FORM_ID = "<firestore-doc-id-from-CMS>";
export async function loadContactForm() {
try { return (await getFormById(CONTACT_FORM_ID)) ?? undefined; }
catch { return undefined; }
}6. Render — load server-side, pass to a "use client" wrapper around <DynamicForm>:
// page.tsx (server component)
const definition = await loadContactForm();
return <ContactForm definition={definition} />;
// ContactForm.tsx ("use client")
import { DynamicForm, type FormDefinition } from "@vulcantech/forms";
export function ContactForm({ definition }: { definition?: FormDefinition }) {
return <DynamicForm definition={definition ?? FALLBACK_DEFINITION} apiRoute="/api/contact" />;
}7. ISR invalidation — ensure src/app/api/revalidate/route.ts exists; the CMS purges form:{projectId}:{formId} (single) and forms:{projectId} (list) on edit.
Required env
VULCANTECH_PROJECT_ID, VULCANTECH_API_KEY (required) · VULCANTECH_CMS_URL (optional) · VULCANTECH_FORMS_NO_CACHE (optional, 1/true to bypass ISR).
Per-app values
- Form definition ID(s) — Firestore doc id from the CMS admin (one per form), pinned in
load<Name>Form.ts(e.g.CONTACT_FORM_ID,SERVICE_REQUEST_FORM_ID).
Notes
FormDefinitionis the CMS-authored schema (sections, fields, submit config, style).style.buttonPlacement: "inline"renders the submit button beside the fields instead of full-width below them — suited to single-field forms (newsletter email + Join). Editable in the CMS form builder under Style → Submit Button → Button Placement.- Newsletter forms (
submissionType: "newsletter") should submit to an app route that proxies to the CMS/api/contact— the CMS routes them intonewsletter_subscribers. - The package also ships
buildYupSchema/buildInitialValuesfor custom render paths, but consumer sites use the server-fetcher pattern above for ISR caching.
