This document explains the Form Builder feature in SaasRock (Core + Pro editions). The Form Builder provides drag-and-drop form creation, AI-powered form generation, conditional logic, and public form URLs with submission actions.
┌─────────────────────────────────────────────────────────────────────────────────┐
│ FORM BUILDER UI │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FORM BUILDER (react-dnd) │ │
│ │ │ │
│ │ ┌───────────┐ ┌─────────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Sidebar │ │ Canvas │ │ Properties Panel │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ + Text │ │ ┌─────────────────┐ │ │ Label: ________ │ │ │
│ │ │ + Email │ │ │ Email Field │ │ │ Required: [x] │ │ │
│ │ │ + Number │ │ └─────────────────┘ │ │ Placeholder: __ │ │ │
│ │ │ + Select │ │ ┌─────────────────┐ │ │ │ │ │
│ │ │ + Radio │ │ │ Message Field │ │ │ Conditional Logic │ │ │
│ │ │ + File │ │ └─────────────────┘ │ │ ________________ │ │ │
│ │ │ + ... │ │ │ │ │ │ │
│ │ └───────────┘ └─────────────────────────┘ └────────────────────┘ │ │
│ │ │ │
│ │ [Preview] [AI Generate] [Save] │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
Save Form JSON
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ DATABASE (Prisma) │
│ │
│ Form ──┬── FormSubmission │
│ │ │
│ └── FormAction (email, webhook, redirect, api) │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
│
Public URL
▼
┌─────────────────────────────────────────────────────────────────────────────────┐
│ PUBLIC FORM ROUTES │
│ │
│ /f/:slug → Global form (admin-created) │
│ /:tenant/f/:slug → Tenant-specific form │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────┐ │
│ │ FORM RENDERER (react) │ │
│ │ │ │
│ │ Multi-step navigation | Conditional visibility | Validation │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
│ │
│ On Submit → Execute Actions → Save Submission │
│ │
└─────────────────────────────────────────────────────────────────────────────────┘
app/modules/formBuilder/
├── lib/
│ ├── types.ts # TypeScript types
│ ├── form-elements.ts # Element definitions
│ ├── colors.ts # Theme colors
│ ├── store.ts # Zustand state management
│ ├── conditional-logic.ts # Condition evaluation
│ ├── templates.ts # Form templates
│ ├── dnd.ts # Drag and drop utilities
│ ├── element-factory.ts # Element creation
│ ├── element-helpers.ts # Element utilities
│ └── variables.ts # Variable substitution
│
├── components/
│ ├── form-builder.tsx # Main builder UI
│ ├── form-canvas.tsx # Drag-drop canvas
│ ├── form-renderer.tsx # Public form renderer
│ ├── form-builder-sidebar.tsx # Element palette
│ ├── form-builder-properties-panel.tsx # Property editor
│ ├── ai-form-generator-dialog.tsx # AI generation
│ ├── create-form-dialog.tsx # New form dialog
│ ├── preview-dialog.tsx # Form preview
│ ├── form-wrapper.tsx # Form container
│ └── elements/ # 25+ element components
│ ├── text-input.tsx
│ ├── email-input.tsx
│ ├── number-input.tsx
│ ├── textarea.tsx
│ ├── select.tsx
│ ├── radio-group.tsx
│ ├── checkbox-group.tsx
│ ├── date-picker.tsx
│ ├── time-picker.tsx
│ ├── file-upload.tsx
│ ├── signature.tsx
│ ├── rating.tsx
│ ├── slider.tsx
│ ├── switch.tsx
│ ├── phone-input.tsx
│ ├── url-input.tsx
│ ├── color-picker.tsx
│ ├── heading.tsx
│ ├── paragraph.tsx
│ ├── divider.tsx
│ ├── spacer.tsx
│ ├── image.tsx
│ ├── hidden-field.tsx
│ ├── page-break.tsx
│ └── ...
│
├── db/
│ └── forms.db.server.ts # Database operations
│
└── services/
├── form-submissions.server.ts # Submission handling
├── form-actions.service.ts # Action execution
└── form-dynamic-options.server.ts # Dynamic options
model Form {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tenantId String? // null = admin/global form
name String
slug String // URL-safe identifier
description String?
elements String // JSON: FormElement[]
settings String // JSON: FormSettings
isActive Boolean @default(true)
isPublic Boolean @default(false)
submissions FormSubmission[]
actions FormAction[]
tenant Tenant? @relation(fields: [tenantId], references: [id], onDelete: Cascade)
@@unique([tenantId, slug]) // Slug unique per tenant
}
model FormSubmission {
id String @id @default(cuid())
createdAt DateTime @default(now())
formId String
data String // JSON: { [fieldId]: value }
metadata String? // JSON: { ip, userAgent, etc. }
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
}
model FormAction {
id String @id @default(cuid())
createdAt DateTime @default(now())
formId String
type String // "email" | "webhook" | "redirect" | "api"
config String // JSON: action-specific configuration
order Int @default(0)
isActive Boolean @default(true)
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
}| Element | Type | Description |
|---|---|---|
| Text Input | text |
Single-line text |
email |
Email with validation | |
| Number | number |
Numeric input |
| Phone | phone |
Phone number input |
| URL | url |
URL with validation |
| Textarea | textarea |
Multi-line text |
| Password | password |
Masked input |
| Element | Type | Description |
|---|---|---|
| Select | select |
Dropdown single-select |
| Multi-Select | multi-select |
Dropdown multi-select |
| Radio Group | radio |
Single choice radio buttons |
| Checkbox Group | checkbox |
Multiple choice checkboxes |
| Switch | switch |
Toggle on/off |
| Element | Type | Description |
|---|---|---|
| Date Picker | date |
Date selection |
| Time Picker | time |
Time selection |
| DateTime | datetime |
Combined date and time |
| Element | Type | Description |
|---|---|---|
| File Upload | file |
Single/multiple file upload |
| Signature | signature |
Canvas for digital signature |
| Rating | rating |
Star rating (1-5) |
| Slider | slider |
Range slider |
| Color Picker | color |
Color selection |
| Hidden Field | hidden |
Hidden value for tracking |
| Element | Type | Description |
|---|---|---|
| Heading | heading |
Section header (h1-h6) |
| Paragraph | paragraph |
Descriptive text |
| Divider | divider |
Horizontal line |
| Spacer | spacer |
Vertical spacing |
| Image | image |
Display image |
| Page Break | page-break |
Multi-step form separator |
Elements can be shown/hidden based on other field values.
| Operator | Description |
|---|---|
equals |
Exact match |
not_equals |
Does not match |
contains |
Contains substring |
not_contains |
Does not contain |
starts_with |
Starts with string |
ends_with |
Ends with string |
is_empty |
Value is empty |
is_not_empty |
Value is not empty |
greater_than |
Numeric comparison |
less_than |
Numeric comparison |
type ConditionalLogic = {
action: "show" | "hide";
logicType: "all" | "any"; // AND or OR
conditions: {
fieldId: string;
operator: ConditionOperator;
value: string | number;
}[];
};Actions execute when a form is submitted.
Send notification email on submission.
{
type: "email",
config: {
to: "admin@example.com", // or {{email}} variable
subject: "New submission: {{form_name}}",
template: "form-submission",
includeData: true
}
}POST submission data to external URL.
{
type: "webhook",
config: {
url: "https://api.example.com/webhook",
method: "POST",
headers: { "X-API-Key": "..." },
includeMetadata: true
}
}Redirect user after submission.
{
type: "redirect",
config: {
url: "/thank-you",
includeSubmissionId: true // ?submissionId=xxx
}
}Call internal SaasRock API.
{
type: "api",
config: {
endpoint: "/api/custom/handler",
method: "POST"
}
}| URL Pattern | Scope | Example |
|---|---|---|
/f/:slug |
Global (admin) | /f/contact-us |
/:tenant/f/:slug |
Tenant-specific | /acme-corp/f/feedback |
type FormSettings = {
submitButtonText: string; // Default: "Submit"
successMessage: string; // After submission message
showProgressBar: boolean; // For multi-step forms
allowMultiple: boolean; // Allow multiple submissions
requireAuth: boolean; // Require login
captcha: boolean; // Enable CAPTCHA
theme: {
primaryColor: string;
backgroundColor: string;
fontFamily: string;
};
};The Form Builder includes AI-powered form generation using OpenAI.
- Click "AI Generate" button in the builder
- Describe the form you want (e.g., "Create a contact form with name, email, message, and department selection")
- AI generates the form structure with appropriate elements
- Review and customize the generated form
The AI generates a complete form JSON including:
- Element types and configuration
- Labels and placeholders
- Validation rules
- Conditional logic suggestions
- Multi-step layout for complex forms
| Route | Purpose |
|---|---|
/admin/forms |
All forms list |
/admin/forms/:id |
Form overview |
/admin/forms/:id/edit |
Form builder |
/admin/forms/:id/submissions |
Submission list |
/admin/forms/:id/actions |
Action configuration |
/admin/forms/:id/analytics |
Form analytics |
| Route | Purpose |
|---|---|
/app/:tenant/settings/forms |
Tenant forms list |
/app/:tenant/settings/forms/:id |
Form overview |
/app/:tenant/settings/forms/:id/edit |
Form builder |
/app/:tenant/settings/forms/:id/preview |
Form preview |
/app/:tenant/settings/forms/:id/share |
Public URL & embed |
/app/:tenant/settings/forms/:id/submissions |
Submissions |
/app/:tenant/settings/forms/:id/analytics |
Analytics |
/app/:tenant/settings/forms/:id/actions |
Actions config |
/app/:tenant/settings/forms/:id/activity |
Activity log |
/app/:tenant/settings/forms/:id/settings |
Form settings |
| Route | Purpose |
|---|---|
/f/:slug |
Global public form |
/:tenant/f/:slug |
Tenant-specific public form |
Form Builder uses Zustand for local state:
type FormBuilderStore = {
elements: FormElement[];
selectedElementId: string | null;
isDragging: boolean;
// Actions
addElement: (element: FormElement) => void;
updateElement: (id: string, updates: Partial<FormElement>) => void;
removeElement: (id: string) => void;
reorderElements: (sourceIndex: number, destIndex: number) => void;
selectElement: (id: string | null) => void;
setDragging: (isDragging: boolean) => void;
};import { createForm } from "~/modules/formBuilder/db/forms.db.server";
const form = await createForm({
tenantId: tenant.id, // or null for global
name: "Contact Form",
slug: "contact",
elements: [
{
id: "name",
type: "text",
label: "Your Name",
required: true,
},
{
id: "email",
type: "email",
label: "Email Address",
required: true,
},
{
id: "message",
type: "textarea",
label: "Message",
required: true,
},
],
settings: {
submitButtonText: "Send Message",
successMessage: "Thank you for contacting us!",
},
});import { FormRenderer } from "~/modules/formBuilder/components/form-renderer";
<FormRenderer
form={form}
onSubmit={async (data) => {
await submitForm(form.id, data);
}}
/>- Define element in
lib/form-elements.ts - Create component in
components/elements/ - Register in element factory
- Add to sidebar palette
Forms can be embedded via iframe:
<iframe
src="https://yourapp.com/f/contact-us"
width="100%"
height="600"
frameborder="0"
></iframe>Or with direct URL link for full-page forms.