Basic Form Example
Learn Form Engine fundamentals with this simple contact form example that demonstrates field configuration, validation, and the actions system.
Interactive Demo
Key Concepts
This basic form demonstrates the essential Form Engine concepts:
1. Field Configuration
Each field requires an id, type, name, and label:
{
id: 'name',
type: 'text',
name: 'name',
label: 'Name',
placeholder: 'Enter your name',
required: true
}2. Validation Rules
Add validation rules to each field:
rules: [
{ type: 'required', message: 'Name is required' },
{ type: 'minLength', value: 2, message: 'Name must be at least 2 characters' },
];3. Actions System
Define how the form behaves when submitted using actions:
actions: [
{
id: 'submit',
$type: 'submit',
label: 'Send Message',
endpoint: '/api/contact',
method: 'POST',
rules:,
onSuccess: {
type: 'message',
title: 'Message Sent!',
message: 'We\'ll get back to you soon.'
}
}
]Complete Configuration
import { FormConfig } from '@/lib/form-engine/types';
const basicFormConfig: FormConfig = {
id: 'basic-contact-form',
title: 'Contact Us',
elements: [
{
id: 'name',
type: 'text',
name: 'name',
label: 'Name',
placeholder: 'Enter your name',
required: true,
rules:
{ type: 'required', message: 'Name is required' },
{ type: 'minLength', value: 2, message: 'Name must be at least 2 characters' }
]
},
{
id: 'email',
type: 'email',
name: 'email',
label: 'Email',
placeholder: '[email protected]',
required: true,
rules:
{ type: 'required', message: 'Email is required' },
{ type: 'email', message: 'Please enter a valid email address' }
]
},
{
id: 'message',
id: 'textarea',
name: 'message',
label: 'Message',
placeholder: 'Tell us how we can help...',
required: true,
props: { rows: 4 },
rules:
{ type: 'required', message: 'Message is required' },
{ type: 'minLength', value: 10, message: 'Message must be at least 10 characters' }
]
}
],
actions: [
{
id: 'submit',
$type: 'submit',
label: 'Send Message',
endpoint: '/api/contact',
method: 'POST',
rules:,
onSuccess: {
type: 'message',
title: 'Message Sent!',
message: 'We\'ll get back to you soon.'
},
onError: {
type: 'message',
title: 'Error',
message: 'Failed to send message. Please try again.',
variant: 'destructive'
}
}
]
};Usage
import { FormRenderer } from '@schema-engine/forms-react';
function ContactPage() {
return (
<div className="max-w-md mx-auto p-6">
<FormRenderer config={basicFormConfig} />
</div>
);
}Next Steps
- Field Types - Explore all available field types
- Validation - Learn about validation rules
- Contact Form - See a more complex example
- Multi-Step Forms - Build wizards and surveys
// src/configs/forms/contact-form.ts
import type { FormConfig } from "@/lib/form-engine";
export const contactFormConfig: FormConfig = {
id: "contact-form",
title: "Get in Touch",
description: "We'd love to hear from you. Send us a message and we'll respond as soon as possible.",
steps: [
{
title: "Contact Information",
elements: [
{
type: "input",
name: "firstName",
label: "First Name",
placeholder: "Enter your first name",
rules:
{ type: "required", message: "First name is required" },
{ type: "minLength", value: 2, message: "Must be at least 2 characters" }
],
grid: { colSpan: 1 }
},
{
type: "input",
name: "lastName",
label: "Last Name",
placeholder: "Enter your last name",
rules:
{ type: "required", message: "Last name is required" },
{ type: "minLength", value: 2, message: "Must be at least 2 characters" }
],
grid: { colSpan: 1 }
},
{
type: "input",
name: "email",
label: "Email Address",
inputType: "email",
placeholder: "[email protected]",
autoComplete: "email",
rules:
{ type: "required", message: "Email is required" },
{ type: "email", message: "Please enter a valid email address" }
],
grid: {
colSpan: 2,
responsive: {
sm: { colSpan: 1 }
}
}
},
{
type: "input",
name: "phone",
label: "Phone Number",
inputType: "tel",
placeholder: "+1 (555) 123-4567",
autoComplete: "tel",
rules:
{
type: "pattern",
value: "^[\\+]?[1-9][\\d\\s\\-\\(\\)]{7,15}$",
message: "Please enter a valid phone number"
}
],
grid: {
colSpan: 2,
responsive: {
sm: { colSpan: 1 }
}
}
},
{
type: "select",
name: "subject",
label: "Subject",
placeholder: "What is this regarding?",
options: [
{ value: "general", label: "General Inquiry" },
{ value: "support", label: "Technical Support" },
{ value: "sales", label: "Sales Question" },
{ value: "partnership", label: "Partnership Opportunity" },
{ value: "feedback", label: "Feedback" },
{ value: "other", label: "Other" }
],
rules:
{ type: "required", message: "Please select a subject" }
],
grid: { colSpan: 2 }
},
{
type: "textarea",
name: "message",
label: "Message",
placeholder: "Tell us about your project, question, or how we can help you...",
rows: 5,
rules:
{ type: "required", message: "Message is required" },
{ type: "minLength", value: 10, message: "Please provide at least 10 characters" },
{ type: "maxLength", value: 1000, message: "Message cannot exceed 1000 characters" }
],
grid: { colSpan: 2 }
},
{
type: "select",
name: "preferredContact",
label: "Preferred Contact Method",
options: [
{ value: "email", label: "Email" },
{ value: "phone", label: "Phone" },
{ value: "either", label: "Either is fine" }
],
defaultValue: "email",
grid: { colSpan: 1 }
},
{
type: "select",
name: "urgency",
label: "Urgency",
options: [
{ value: "low", label: "Low - Response within 5 business days" },
{ value: "medium", label: "Medium - Response within 2 business days" },
{ value: "high", label: "High - Response within 1 business day" },
{ value: "urgent", label: "Urgent - Response within 4 hours" }
],
defaultValue: "medium",
grid: { colSpan: 1 }
},
{
type: "checkbox",
name: "newsletter",
label: "Subscribe to our newsletter",
description: "Get updates about new features, tips, and industry insights",
checked: false,
grid: { colSpan: 2 }
},
{
type: "checkbox",
name: "privacy",
label: "I agree to the Privacy Policy and Terms of Service",
rules:
{
type: "custom",
message: "You must agree to the privacy policy to continue",
validator: (value) => value === true
}
],
grid: { colSpan: 2 }
}
]
}
],
// Responsive grid layout
grid: {
columns: 1,
gap: 24,
responsive: {
sm: {
columns: 2,
gap: 20
},
lg: {
gap: 24
}
}
},
// Auto-generate Zod schema from validation rules
// Form actions
actions: [
{
id: "submit-contact",
$type: "submit",
endpoint: "/api/contact",
method: "POST",
successMessage: "Thank you for your message! We'll get back to you soon.",
errorMessage: "Sorry, there was an error sending your message. Please try again.",
triggers: ["form_submit"]
},
{
id: "success-notification",
$type: "show_notification",
message: "Your message has been sent successfully!",
variant: "success",
duration: 5000,
triggers: ["submit_success"]
},
{
id: "track-submission",
$type: "gtm_event",
eventName: "contact_form_submitted",
eventData: {
form_id: "contact-form",
form_name: "Contact Us"
},
includeMetadata: true,
triggers: ["submit_success"]
}
]
};Implementation Component
Here's how to implement the form in a React component:
// src/components/ContactForm.tsx
import { FormRenderer } from "@/lib/form-engine";
import { contactFormConfig } from "@/configs/forms/contact-form";
import { useState } from "react";
import { toast } from "react-hot-toast";
interface ContactFormData {
firstName: string;
lastName: string;
email: string;
phone?: string;
subject: string;
message: string;
preferredContact: "email" | "phone" | "either";
urgency: "low" | "medium" | "high" | "urgent";
newsletter: boolean;
privacy: boolean;
}
export function ContactForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (data: ContactFormData) => {
setIsSubmitting(true);
try {
// Transform data for API
const payload = {
...data,
fullName: `${data.firstName} ${data.lastName}`,
timestamp: new Date().toISOString(),
source: "website_contact_form"
};
// Submit to API
const response = await fetch("/api/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error("Failed to submit form");
}
const result = await response.json();
// Success handling
toast.success("Thank you for your message! We'll get back to you soon.");
// Analytics tracking
if (typeof gtag !== 'undefined') {
gtag('event', 'contact_form_submitted', {
event_category: 'engagement',
event_label: data.subject,
value: 1
});
}
} catch (error) {
console.error("Form submission error:", error);
toast.error("Sorry, there was an error sending your message. Please try again.");
} finally {
setIsSubmitting(false);
}
};
const handleChange = (data: Partial<ContactFormData>) => {
// Optional: Handle form changes for auto-save, analytics, etc.
console.log("Form data changed:", data);
};
return (
<div className="max-w-4xl mx-auto p-6">
<div className="mb-8 text-center">
<h1 className="text-3xl font-bold text-gray-900 mb-4">
{contactFormConfig.title}
</h1>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
{contactFormConfig.description}
</p>
</div>
<div className="bg-white rounded-lg shadow-lg p-8">
<FormRenderer
config={contactFormConfig}
onSubmit={handleSubmit}
onChange={handleChange}
className="space-y-6"
/>
</div>
{isSubmitting && (
<div className="fixed inset-0 bg-black bg-opacity/50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 flex items-center space-x-4">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600"></div>
<span className="text-gray-700">Sending your message...</span>
</div>
</div>
)}
</div>
);
}Page Integration
Integrate the form into a page:
// src/pages/contact.tsx
import { ContactForm } from "@/components/ContactForm";
import { BaseLayout } from "@/layouts/base-layout";
export default function ContactPage() {
return (
<BaseLayout>
<div className="min-h-screen bg-gray-50 py-12">
<ContactForm />
</div>
</BaseLayout>
);
}
export const getMetadata = () => ({
title: "Contact Us | Your Company",
description: "Get in touch with our team. We're here to help with any questions or feedback you may have.",
openGraph: {
title: "Contact Us",
description: "Get in touch with our team",
type: "website"
}
});API Handler
Backend API handler for form submission:
// src/pages/api/contact.ts (Next.js API route)
import type { NextApiRequest, NextApiResponse } from 'next';
import { z } from 'zod';
// Validation schema
const ContactFormSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
phone: z.string().optional(),
subject: z.enum(['general', 'support', 'sales', 'partnership', 'feedback', 'other']),
message: z.string().min(10).max(1000),
preferredContact: z.enum(['email', 'phone', 'either']),
urgency: z.enum(['low', 'medium', 'high', 'urgent']),
newsletter: z.boolean(),
privacy: z.boolean().refine((val) => val === true, {
message: 'Privacy policy must be accepted',
}),
fullName: z.string(),
timestamp: z.string(),
source: z.string(),
});
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') {
return res.status(405).json({ error: 'Method not allowed' });
}
try {
// Validate request data
const data = ContactFormSchema.parse(req.body);
// Process the form submission
await processContactForm(data);
// Send notification emails
await sendNotifications(data);
// Add to CRM/database
await saveToDatabase(data);
return res.status(200).json({
success: true,
message: 'Contact form submitted successfully',
});
} catch (error) {
console.error('Contact form error:', error);
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Invalid form data',
details: error.errors,
});
}
return res.status(500).json({
error: 'Internal server error',
});
}
}
async function processContactForm(data: z.infer<typeof ContactFormSchema>) {
// Your business logic here
console.log('Processing contact form:', data);
}
async function sendNotifications(data: z.infer<typeof ContactFormSchema>) {
// Send email notifications
// Could use SendGrid, Mailgun, etc.
}
async function saveToDatabase(data: z.infer<typeof ContactFormSchema>) {
// Save to your database
// Could use Prisma, Supabase, etc.
}Styling and Customization
Custom styling for the contact form:
/* src/styles/contact-form.css */
.contact-form {
@apply max-w-4xl mx-auto;
}
.contact-form .form-field {
@apply transition-all duration-200;
}
.contact-form .form-field:focus-within {
@apply transform scale-[1.02];
}
.contact-form .form-label {
@apply font-medium text-gray-700;
}
.contact-form .form-input {
@apply border-gray-300 focus:border-blue-500 focus:ring-blue-500;
}
.contact-form .form-textarea {
@apply resize-none;
}
.contact-form .form-select {
@apply cursor-pointer;
}
.contact-form .form-checkbox {
@apply text-blue-600 focus:ring-blue-500;
}
.contact-form .form-error {
@apply text-red-600 text-sm mt-1;
}
.contact-form .submit-button {
@apply w-full bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition-colors duration-200;
}
.contact-form .submit-button:disabled {
@apply opacity-50 cursor-not-allowed;
}Testing
Unit tests for the contact form:
// src/components/__tests__/ContactForm.test.tsx
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ContactForm } from '../ContactForm';
// Mock the API call
global.fetch = jest.fn();
describe('ContactForm', () => {
beforeEach(() => {
(fetch as jest.Mock).mockClear();
});
it('renders all form fields', () => {
render(<ContactForm />);
expect(screen.getByLabelText(/first name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/last name/i)).toBeInTheDocument();
expect(screen.getByLabelText(/email address/i)).toBeInTheDocument();
expect(screen.getByLabelText(/subject/i)).toBeInTheDocument();
expect(screen.getByLabelText(/message/i)).toBeInTheDocument();
});
it('validates required fields', async () => {
const user = userEvent.setup();
render(<ContactForm />);
const submitButton = screen.getByRole('button', { name: /submit/i });
await user.click(submitButton);
await waitFor(() => {
expect(screen.getByText(/first name is required/i)).toBeInTheDocument();
expect(screen.getByText(/email is required/i)).toBeInTheDocument();
});
});
it('submits form with valid data', async () => {
const user = userEvent.setup();
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ success: true })
});
render(<ContactForm />);
// Fill out form
await user.type(screen.getByLabelText(/first name/i), 'John');
await user.type(screen.getByLabelText(/last name/i), 'Doe');
await user.type(screen.getByLabelText(/email address/i), '[email protected]');
await user.selectOptions(screen.getByLabelText(/subject/i), 'general');
await user.type(screen.getByLabelText(/message/i), 'This is a test message');
await user.click(screen.getByLabelText(/privacy policy/i));
// Submit form
await user.click(screen.getByRole('button', { name: /submit/i }));
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/contact', expect.objectContaining({
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: expect.stringContaining('[email protected]')
}));
});
});
});Key Features Demonstrated
This example showcases:
- Complete Form Configuration: All field types, validation, and layout
- Responsive Design: Mobile-first grid layout that adapts to screen size
- Comprehensive Validation: Required fields, format validation, custom validation
- Form Actions: Submit handling, notifications, analytics tracking
- Error Handling: Graceful error handling and user feedback
- Accessibility: Proper labels, ARIA attributes, keyboard navigation
- Type Safety: Full TypeScript integration with type inference
- Testing: Complete test coverage for form behavior
Customization Options
You can easily customize this example:
- Fields: Add, remove, or modify field types and validation
- Layout: Adjust grid configuration for different layouts
- Styling: Apply custom CSS classes and themes
- Actions: Add auto-save, different submission endpoints, etc.
- Validation: Custom validation rules and error messages
- Integration: Connect to different APIs, CRMs, or databases
This basic form example provides a solid foundation that you can extend for your specific needs while maintaining the benefits of Form Engine's declarative approach.
Next Steps
- Contact Form Example - Enhanced version with more features
- Registration Flow Example - Multi-step user registration
- Survey Form Example - Complex survey with conditional logic
- Field Types - Learn about all available field types