obsidian/wiki/payloadcms/plugin-form-builder.md
2026-05-15 16:16:30 +01:00

205 lines
7.1 KiB
Markdown

---
title: "Form Builder Plugin"
aliases: [form-builder, plugin-form-builder, payloadcms-forms]
tags: [payloadcms, plugin, forms, email, payments, uploads]
sources: [raw/plugins__form-builder.md]
created: 2026-05-15
updated: 2026-05-15
---
## Overview
`@payloadcms/plugin-form-builder` lets admins build and manage forms dynamically from the Admin Panel — no hard-coding new forms in the codebase. The front-end maps over the schema and renders its own UI components. All submissions are stored in the DB; confirmations can be on-screen messages or redirects; dynamic emails are sent on submission.
Replaces third-party services (HubSpot, Mailchimp) with first-party tooling.
## Installation
```bash
pnpm add @payloadcms/plugin-form-builder
```
```ts
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
export default buildConfig({
plugins: [formBuilderPlugin({ /* options */ })],
})
```
## Key Options
| Option | Type | Purpose |
|--------|------|---------|
| `fields` | object | Enable/disable/override individual field types |
| `redirectRelationships` | string[] | Collection slugs available as redirect targets |
| `beforeEmail` | hook | Transform emails just before they are sent |
| `defaultToEmail` | string | Fallback recipient if form config omits `to` |
| `formOverrides` | CollectionConfig | Override the `forms` collection (slug, access, fields) |
| `formSubmissionOverrides` | CollectionConfig | Override the `form-submissions` collection |
| `handlePayment` | hook | Process payment on submission (use with `payment` field) |
| `uploadCollections` | string[] | Required when `upload` field is enabled |
### Security Warning
The `forms` collection is **publicly readable by default** — including the `emails` field. Override `formOverrides.access` to restrict it, especially if you have frontend users.
```ts
formBuilderPlugin({
formOverrides: {
access: {
read: ({ req: { user } }) => !!user,
},
fields: ({ defaultFields }) => [...defaultFields, { name: 'custom', type: 'text' }],
},
})
```
## Available Field Types
| Field | Maps to | Notes |
|-------|---------|-------|
| `text` | `<input type="text">` | Simple string |
| `textarea` | `<textarea>` | Multiline string |
| `select` | `<select>` | Options array with label/value |
| `radio` | radio group | Options array with label/value |
| `email` | `<input type="email">` | Email with format validation |
| `state` | `<select>` | US states |
| `country` | `<select>` | Countries |
| `checkbox` | `<input type="checkbox">` | Boolean |
| `date` | `<input type="date">` | UTC stored; timezone configurable |
| `number` | `<input type="number">` | Numeric |
| `message` | RichText | Display-only content block in form |
| `payment` | — | Triggers `handlePayment` on submit; disabled by default |
| `upload` | file input | Disabled by default; requires `uploadCollections` config |
All fields share common properties: `name`, `label`, `defaultValue`, `width`, `required`.
### Field Overrides
```ts
import { fields } from '@payloadcms/plugin-form-builder'
formBuilderPlugin({
fields: {
text: {
...fields.text,
labels: { singular: 'Custom Text Field', plural: 'Custom Text Fields' },
},
date: false, // disable
},
})
```
### GraphQL Name Collision Fix
If your own blocks/collections use the same names as plugin fields (e.g. `Country`):
```ts
formBuilderPlugin({
fields: {
country: { interfaceName: 'CountryFormBlock' },
},
})
```
## Upload Fields
Disabled by default. Enable and configure upload collections:
```ts
formBuilderPlugin({
fields: {
upload: {
uploadCollections: ['media', 'documents'], // required
},
},
})
```
**Frontend submission via `multipart/form-data`:**
```ts
const formData = new FormData()
formData.append(field.name, fileValue) // upload fields
formData.append('_payload', JSON.stringify({ form: formId, submissionData }))
await fetch('/api/form-submissions', { method: 'POST', body: formData })
```
The server validates MIME types/sizes, uploads to the collection, and stores file IDs.
**Alternative: pre-upload then submit ID** — upload file first via `POST /api/{collection}`, then pass the returned `doc.id` as the field value.
**Presigned URL flow** (for large files with S3/GCS/Azure `clientUploads` enabled):
1. `POST /api/storage-s3-generate-signed-url` → get presigned URL
2. `PUT` file directly to cloud storage
3. `POST /api/{collection}` with metadata to create the DB document
## Payment Integration
```ts
import { getPaymentTotal } from '@payloadcms/plugin-form-builder'
formBuilderPlugin({
handlePayment: async ({ form, submissionData }) => {
const paymentField = form.fields?.find(f => f.blockType === 'payment')
const price = getPaymentTotal({
basePrice: paymentField.basePrice,
priceConditions: paymentField.priceConditions,
fieldValues: submissionData,
})
// call your payment processor here
},
})
```
`priceConditions` define conditional pricing rules (field → condition → operator → value → delta).
## Email
Uses Payload's [[wiki/payloadcms/email|email configuration]] (Nodemailer/Resend). Supports:
- Dynamic field interpolation: `Thank you, {{name}}!`
- `{{*}}` wildcard — outputs all fields as `key: value`
- `{{*:table}}` — outputs all fields as HTML table
- Rich text body serialized to HTML server-side
**`beforeEmail` hook** — inject HTML templates before sending:
```ts
beforeEmail: (emailsToSend) =>
emailsToSend.map(email => ({ ...email, html: wrapTemplate(email.html) }))
```
**SendGrid gotcha:** if using Link Branding + Domain Authentication, the `from` address must be on your verified domain. `from: {{email}}` will fail — use `website@your_domain.com` instead.
## TypeScript
```ts
import type {
PluginConfig, Form, FormSubmission,
FieldsConfig, BeforeEmail, HandlePayment,
UploadField, UploadFieldMimeType,
} from '@payloadcms/plugin-form-builder/types'
```
## Key Takeaways
- Install `@payloadcms/plugin-form-builder`, add to `plugins[]` — creates `forms` and `form-submissions` collections automatically
- `forms` collection is **world-readable by default** — override `formOverrides.access` in production
- Upload field is **disabled by default** — enable it and set `uploadCollections`
- Payment field calls `handlePayment` hook; use `getPaymentTotal()` for conditional pricing
- Email uses Payload's email config with `{{fieldName}}` interpolation; SendGrid domain auth restricts `from` address
- GraphQL type name collisions from plugin fields → fix with `interfaceName` override
- `form-submissions` allows public `create` but blocks `update` and `read` by default (correct for public forms)
## Related
- [[wiki/payloadcms/plugins|Official Plugins]] — all 10 plugins overview
- [[wiki/payloadcms/email|Email — Adapters]] — Nodemailer, Resend, attachments
- [[wiki/payloadcms/upload|Upload & Media]] — upload collections, storage adapters
- [[wiki/payloadcms/hooks-collections|Collection Hooks]] — beforeChange, afterChange patterns
- [[wiki/payloadcms/authentication-overview|Authentication Overview]] — access control on collections
## Sources
- `raw/plugins__form-builder.md` — official Payload CMS Form Builder Plugin docs