Skip to content

Instantly share code, notes, and snippets.

@AlexandroMtzG
Created January 12, 2026 02:58
Show Gist options
  • Select an option

  • Save AlexandroMtzG/0a2721a8386212f47d1c0f029f230637 to your computer and use it in GitHub Desktop.

Select an option

Save AlexandroMtzG/0a2721a8386212f47d1c0f029f230637 to your computer and use it in GitHub Desktop.

Form Builder Architecture

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.


Overview

┌─────────────────────────────────────────────────────────────────────────────────┐
│                              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                                  │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘

Module Structure

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

Database Schema

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)
}

Form Elements

Input Elements

Element Type Description
Text Input text Single-line text
Email 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

Selection Elements

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

Date/Time Elements

Element Type Description
Date Picker date Date selection
Time Picker time Time selection
DateTime datetime Combined date and time

Special Elements

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

Layout Elements

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

Conditional Logic

Elements can be shown/hidden based on other field values.

Condition Types

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

Condition Configuration

type ConditionalLogic = {
  action: "show" | "hide";
  logicType: "all" | "any";  // AND or OR
  conditions: {
    fieldId: string;
    operator: ConditionOperator;
    value: string | number;
  }[];
};

Form Actions

Actions execute when a form is submitted.

Email Action

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
  }
}

Webhook Action

POST submission data to external URL.

{
  type: "webhook",
  config: {
    url: "https://api.example.com/webhook",
    method: "POST",
    headers: { "X-API-Key": "..." },
    includeMetadata: true
  }
}

Redirect Action

Redirect user after submission.

{
  type: "redirect",
  config: {
    url: "/thank-you",
    includeSubmissionId: true  // ?submissionId=xxx
  }
}

API Action

Call internal SaasRock API.

{
  type: "api",
  config: {
    endpoint: "/api/custom/handler",
    method: "POST"
  }
}

Public Form URLs

URL Pattern Scope Example
/f/:slug Global (admin) /f/contact-us
/:tenant/f/:slug Tenant-specific /acme-corp/f/feedback

Form Settings

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;
  };
};

AI Form Generation

The Form Builder includes AI-powered form generation using OpenAI.

Usage

  1. Click "AI Generate" button in the builder
  2. Describe the form you want (e.g., "Create a contact form with name, email, message, and department selection")
  3. AI generates the form structure with appropriate elements
  4. Review and customize the generated form

Generated Structure

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

Routes

Admin Routes

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

Tenant Routes

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

Public Routes

Route Purpose
/f/:slug Global public form
/:tenant/f/:slug Tenant-specific public form

State Management

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;
};

Quick Reference

Creating a Form Programmatically

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!",
  },
});

Rendering a Form

import { FormRenderer } from "~/modules/formBuilder/components/form-renderer";

<FormRenderer
  form={form}
  onSubmit={async (data) => {
    await submitForm(form.id, data);
  }}
/>

Adding a Custom Element Type

  1. Define element in lib/form-elements.ts
  2. Create component in components/elements/
  3. Register in element factory
  4. Add to sidebar palette

Embedding Forms

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment