Dynamic Forms
New FeatureUse forms that automatically sync with the admin interface - no more copy-pasting code!
❌ Before (Old Way)
- ❌ Two files: page.tsx + client.tsx
- ❌ Copy generated code from admin
- ❌ Paste into client.tsx
- ❌ Re-copy when form changes
- ❌ Risk of code getting out of sync
- ❌ Manual server-side logging setup
✅ Now (Dynamic Forms)
- ✅ One file: just page.tsx
- ✅ One line of code:
<DynamicForm slug="form-name" />
- ✅ Automatically syncs with admin changes
- ✅ Server-side form loading & logging
- ✅ Always up-to-date
- ✅ No maintenance required
import DynamicForm from '@/components/DynamicForm'
export default function ContactPage() {
return (
<div className="max-w-md mx-auto">
<h1>Contact Us</h1>
<DynamicForm slug="contact-us" />
</div>
)
}
import DynamicForm from '@/components/DynamicForm'
import { useRouter } from 'next/navigation'
export default function NewsletterPage() {
const router = useRouter()
return (
<div className="max-w-lg mx-auto">
<DynamicForm
slug="newsletter-signup"
theme="shadcn"
className="space-y-6"
containerClassName="bg-white p-6 rounded-lg shadow-md"
recaptcha={true}
onSuccess={(result) => {
console.log('Newsletter signup successful!', result)
// Handle different confirmation types
if (result.confirmation?.type === 'redirect') {
router.push(result.confirmation.redirectUrl!)
} else {
// Show success message (handled automatically)
}
}}
onError={(error) => {
console.error('Newsletter signup failed:', error)
// Custom error handling
}}
onSubmit={(data) => {
console.log('Form data being submitted:', data)
// Access form data before submission
}}
/>
</div>
)
}
shadcn
Uses shadcn/ui components (recommended)
- ✅ Consistent with your UI
- ✅ Accessible components
- ✅ Professional styling
html
Plain HTML with Tailwind classes
- ✅ No dependencies
- ✅ Lightweight
- ✅ Easy to customize
custom
Minimal markup for custom styling
- ✅ Full control
- ✅ CSS classes for targeting
- ✅ Custom renderers
import DynamicForm from '@/components/DynamicForm'
import { FormField } from '@/feature/form/formTypes'
function CustomFieldRenderer(field: FormField, index: number) {
if (field.type === 'email') {
return (
<div key={index} className="my-custom-email-field">
<label className="fancy-label">{field.label}</label>
<input
type="email"
name={field.name}
required={field.required}
className="fancy-input"
placeholder={`Enter your ${field.label.toLowerCase()}`}
/>
</div>
)
}
// Return null to use default renderer for other field types
return null
}
export default function CustomFormPage() {
return (
<DynamicForm
slug="custom-styled-form"
theme="custom"
customFieldRenderer={CustomFieldRenderer}
/>
)
}
Prop | Type | Default | Description |
---|---|---|---|
slug | string | - | Required. The form slug from admin |
theme | 'shadcn' | 'html' | 'custom' | 'shadcn' | Styling theme to use |
className | string | '' | CSS classes for the form element |
containerClassName | string | '' | CSS classes for the container div |
recaptcha | boolean | false | Enable reCAPTCHA integration |
onSuccess | function | - | Called when form submits successfully |
onError | function | - | Called when form submission fails |
onSubmit | function | - | Called before submission with form data |
customFieldRenderer | function | - | Custom function to render specific fields |
This will show an error if you don't have a form with slug "contact-us" - that's normal for this demo.
❌ Before (Old Pattern)
src/app/forms/contact/page.tsx
import createLog from '@/feature/logs/createLog'
import ContactForm from './client'
import db from '@/feature/db/db'
export default async function ContactPage() {
await createLog({
db,
eventType: 'form.viewed',
metadata: {
formId: '400ee4e2-761e-41de-913b-9a17f636e0c3',
slug: 'contact',
},
})
return <ContactForm />
}
src/app/forms/contact/client.tsx
'use client'
import { useState } from 'react'
import { Input } from '@/components/ui/input'
// ... lots of imports and form JSX code
// ... 94 lines of generated code ...
✅ After (Dynamic Pattern)
src/app/forms/contact/page.tsx
import DynamicForm from '@/components/DynamicForm'
export default async function ContactPage() {
return (
<div className="max-w-md mx-auto">
<h1>Contact Us</h1>
<DynamicForm slug="contact" />
</div>
)
}
That's it! Server-side logging happens automatically inside DynamicForm.
Migration Steps
Step 1: Identify Your Forms
Find form directories with the old pattern: page.tsx
(with createLog call) + client.tsx
(with form JSX and requestSubmitForm).
Step 2: Note the Form Slug
In your client.tsx
, find the formSlug
value passed to requestSubmitForm
.
Step 3: Replace Both Files
Delete client.tsx
and replace your page.tsx
content with just <DynamicForm slug="your-slug" />
. Server-side logging happens automatically.
Step 4: Add Event Handlers (Optional)
If you had custom success/error handling, move it to the onSuccess
and onError
props.
Step 5: Test & Deploy
Test that your form works the same way. Now you can edit the form in admin and it will automatically update!