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

// 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:

  1. Complete Form Configuration: All field types, validation, and layout
  2. Responsive Design: Mobile-first grid layout that adapts to screen size
  3. Comprehensive Validation: Required fields, format validation, custom validation
  4. Form Actions: Submit handling, notifications, analytics tracking
  5. Error Handling: Graceful error handling and user feedback
  6. Accessibility: Proper labels, ARIA attributes, keyboard navigation
  7. Type Safety: Full TypeScript integration with type inference
  8. 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