Create custom forms with the official Form Builder Plugin

Published On
Payload Form Builder Plugin
Payload Form Builder Plugin

If you’re anything like me, you’re tired of dealing with microservices hell.

When building a new website, finding a forms provider can feel like just one more hassle to deal with.

Not to mention getting forms to render properly on your site with an iframe is a straight up nightmare. 

If you're using HubSpot’s iframe forms, customization options are limited, making it difficult to match the look and feel of your website. The result? Disjointed and out-of-place looking forms.

If you try rendering Wordpress’ Gravity Forms on a headless website, you’ll be faced with HTML spaghetti and a complicated API. All in all, this leads to a complex setup and usage that can be overwhelming.

That’s why we developed Payload’s Form Builder plugin. With full control over your front-end and a consistent API, you can avoid exhausting tasks and get the functionality you need out of the box without any headaches.

Plus, you don’t have to pay like you do for Gravity Forms or worry about embedding any iframes. Our form builder plugin gives you complete control over your design, so you can create custom forms that match your website seamlessly. 

Our goal is to make life easier for engineers who have been dealing with this painful process for years. We want to provide a straightforward plugin that gives you all the freedom to create any custom form you need.

How it works

Once you’ve installed and added the plugin to your Payload instance, you’ll see two new collections: Forms and Form Submissions.

The Forms collection is where you’ll store all your different forms, while the Form Submissions collection is where all form submissions will be stored. This allows you to keep track of all form submissions in one place and view them easily when needed.

Forms Collection

The bulk of your form creation tasks will be carried out within the Forms collection. It’s built on the blocks field type, where each block represents a different field type (such as select, text, and checkbox).

If you configure a payment processor like Stripe, you can also use the payment field type to accept payments directly within your forms.

In addition to the standard form fields, you can also create completely custom and dynamic emails using the Forms collection. This allows you to personalize the emails sent to users when they submit a form and tailor them to your specific needs.

forms-collection
Form Submissions Collection

Our new submissions collection is designed to accept JSON submissions and automatically validate the incoming data against the corresponding form. This ensures that all data submitted through the form is accurate and formatted correctly.

form-submissions
Payments / Emails

One of our unique field types is the Payment field type, which gives you complete flexibility when adding payment fields to your forms. This field comes with a variety of options to ensure that your payment processing works smoothly and seamlessly.

payment-fields

Our plugin also integrates smoothly with our email configuration, which is defined in your payload.init() method inside your server.ts file. Once the email configuration is set up, the plugin will read from the config and handle all emails being sent. This makes it easy to manage all email communications related to your forms, without having to work out manually configuring email settings. You can find out more about email configuration in Payload's email documentation.

Demo

To see the simplicity of the form builder plugin in action, you can easily launch the example demo from Payload’s repository, located in the examples folder.

Form Builder CMS Implementation

To use the plugin in your CMS, simply import the package into your Payload config file and add it to your plugins array.

1
import { buildConfig } from 'payload/config';
2
import path from 'path';
3
import FormBuilder from '@payloadcms/plugin-form-builder';
4
import { Users } from './collections/Users';
5
import { Pages } from './collections/Pages';
6
import { MainMenu } from './globals/MainMenu';
7
8
export default buildConfig({
9
collections: [
10
Pages,
11
Users,
12
],
13
globals: [
14
MainMenu,
15
],
16
cors: [
17
'http://localhost:3000',
18
process.env.PAYLOAD_PUBLIC_SITE_URL,
19
],
20
typescript: {
21
outputFile: path.resolve(__dirname, 'payload-types.ts'),
22
},
23
plugins: [
24
FormBuilder({
25
fields: {
26
payment: false,
27
},
28
29
}),
30
],
31
});

Once you’ve installed the plugin, you’ll gain access to a wide range of fields. If you want to customize the default settings, you can pass a boolean value or a partial Payload Block keyed to the block slug. This will allow you to tailor the plugin to your specific needs.

1
fields: {
2
text: true,
3
textarea: true,
4
select: true,
5
email: true,
6
state: true,
7
country: true,
8
checkbox: true,
9
number: true,
10
message: true,
11
payment: false
12
}

Next, we’ll create a straightforward Form Block that establishes a relationship with the form plugin, giving editors complete freedom to add forms to any page of your website. To achieve this, you need to create a relationship field within your Block component and set the relationTo property to forms.

1
import { Block } from 'payload/types';
2
import richText from '../../fields/richText';
3
4
export const FormBlock: Block = {
5
slug: 'formBlock',
6
labels: {
7
singular: 'Form Block',
8
plural: 'Form Blocks',
9
},
10
graphQL: {
11
singularName: 'FormBlock',
12
},
13
fields: [
14
{
15
name: 'form',
16
type: 'relationship',
17
relationTo: 'forms',
18
required: true,
19
},
20
{
21
name: 'enableIntro',
22
label: 'Enable Intro Content',
23
type: 'checkbox',
24
},
25
richText({
26
name: 'introContent',
27
label: 'Intro Content',
28
admin: {
29
condition: (_, { enableIntro }) => Boolean(enableIntro),
30
},
31
}),
32
],
33
};

Finally, you’ll need to configure a Pages collection file that imports your various Blocks.

1
import { CollectionConfig } from 'payload/types';
2
import { publishedOnly } from '../access/publishedOnly';
3
import { FormBlock } from '../blocks/Form';
4
import { slugField } from '../fields/slug';
5
6
export const Pages: CollectionConfig = {
7
slug: 'pages',
8
admin: {
9
useAsTitle: 'title',
10
defaultColumns: ['title', 'slug', 'updatedAt'],
11
},
12
versions: {
13
drafts: true,
14
},
15
access: {
16
read: publishedOnly,
17
},
18
fields: [
19
{
20
name: 'title',
21
type: 'text',
22
required: true,
23
},
24
{
25
type: 'tabs',
26
tabs: [
27
{
28
label: 'Content',
29
fields: [
30
{
31
name: 'layout',
32
type: 'blocks',
33
required: true,
34
blocks: [
35
FormBlock,
36
],
37
},
38
],
39
},
40
],
41
},
42
slugField(),
43
],
44
};

You are now equipped to create forms and add them to your pages in Payload. The admin UI for creating forms is sleek and easy to navigate.

Simply give your form a title and add your desired fields.

field-example-dark-mode

Next, fill out your submit button label. You can then select your preferred confirmation type: Message to display a message on the screen after submission, or Redirect to redirect the user to an internal or external link.

submit-dark-mode

Additionally, you have the option to send an email upon submission.

Once your form is submitted from your site, the data will be sent to the Form Submissions collection.

submission-example
Emails

As previously mentioned, you have the capability to send emails upon form submission with ease. Minimal configuration is required to get started.

Payload uses NodeMailer for emails, which is an industry-standard library that will not pose any issues for those who are already familiar with it.

The settings you provide will be included in the email property object of your payload init call.

1
await payload.init({
2
secret: process.env.PAYLOAD_SECRET,
3
mongoURL: process.env.MONGODB_URI,
4
express: app,
5
email: {
6
fromName: 'Admin',
7
fromAddress: 'admin@example.com',
8
logMockCredentials: true,
9
},
10
onInit: () => {
11
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
12
},
13
});

You can learn more about the configuration process in Payload’s use an email service documentation.

By default, Payload uses a mock implementation that sends emails only to the ethereal capture service, which will never reach the recipient’s inbox. To view the ethereal credentials, add logMockCredentials: true to the email options. This will log the credentials to the console upon startup.

email-login-credentials

Once you’ve properly configured the email settings, navigate to the Email section on the form creating page and provide the necessary details as shown below.

emails-fill

If you’d like to include the field data from the forms within your emails, simply wrap the form field values in double brackets and input them into the Email fields above (most likely in the message field).

For instance, if your form includes fields such as name, email, and phone, and you’d like to display this information in your emails, your email message field should resemble the following:

message

At this point, after you’ve built out the front-end for form submission, you can visit https://ethereal.email and log in with the credentials obtained from your console.log to see the email sent in response to the form submission on your site.

Form Builder Front-End Implementation

Now that your structured data is implemented on your Payload backend, it’s time to start fetching data from the frontend. To begin, create a FormBlock folder that contains essential components such as your FormBlock component and all of your form field components. You can find an example of this in the Blocks folder of the example repository.

To send your form data back to Payload, you need to create a submit function that’s triggered when the user clicks a button.

In the onSubmit function, the first step is to properly format the data for the POST request that will be sent to Payload. This data corresponds to the form fields that are filled out by the user. Here’s an example of how the data gets formatted:

1
const onSubmit = useCallback((data: Data) => {
2
let loadingTimerID: NodeJS.Timer
3
4
const submitForm = async () => {
5
setError(undefined)
6
7
const dataToSend = Object.entries(data).map(([name, value]) => ({
8
field: name,
9
value,
10
}))

Once we have our data formatted, we can send a POST request to the /api/form-submissions endpoint in Payload.

1
const req = await fetch(`${process.env.NEXT_PUBLIC_CMS_URL}/api/form-submissions`, {
2
method: 'POST',
3
headers: {
4
'Content-Type': 'application/json',
5
},
6
body: JSON.stringify({
7
form: formID,
8
submissionData: dataToSend,
9
}),
10
})
11
12
const res = await req.json()

This data is then stored in the Form Submissions collection.

If the selected confirmation type is a redirect, you must handle the redirect on the frontend. Access the URL allocated to the redirect object and then perform the redirect with router.push().

1
if (confirmationType === 'redirect' && redirect) {
2
const { url } = redirect
3
4
const redirectUrl = url
5
6
if (redirectUrl) router.push(redirectUrl)
7
}

Now that you have easy access to your form data, the next step is to build out your form field components with complete customization freedom. Once you have your individual form field components created, you can create your form component that will map over all the form field components.

Note: This repository leverages the popular react-hook-forms library to provide easy validation and error handling.

We simply need to call the useForm hook to validate our forms with minimal re-renders. 

1
const formMethods = useForm({
2
defaultValues: buildInitialFormState(formFromProps.fields),
3
})
4
const {
5
register,
6
handleSubmit,
7
formState: { errors },
8
control,
9
setValue,
10
getValues,
11
} = formMethods

Once we have defined all the necessary properties for our form, we can pass them to both our form element and individual form field components. By doing so, each field component can access these properties and we can handle validation for each field independently. This approach offers a great deal of flexibility, allowing us to manage validation in a way that best suits our needs. An example code snippet is shown below:

1
<form id={formID} onSubmit={handleSubmit(onSubmit)}>
2
<div className={classes.fieldWrap}>
3
{formFromProps &&
4
formFromProps.fields &&
5
formFromProps.fields.map((field, index) => {
6
const Field: React.FC<any> = fields?.[field.blockType]
7
if (Field) {
8
return (
9
<React.Fragment key={index}>
10
<Field
11
form={formFromProps}
12
{...field}
13
{...formMethods}
14
register={register}
15
errors={errors}
16
control={control}
17
/>
18
</React.Fragment>
19
)
20
}
21
return null
22
})}
23
</div>
24
<Button label={submitButtonLabel} appearance="primary" el="button" form={formID} />
25
</form>

Wrap up

Adding a form builder to your website is important, but it can be a daunting task for developers. By using this plugin, you can simplify the process and save time by creating and managing forms easily and efficiently.

Best part? Once you have this all wired up for one site, you can re-use everything you've built for every site you ever need to build from this point on. Investing time upfront to do it right will 10x your dev time from there on out.

We hope to see you all taking advantage of our plugin. Have a great day!

Learn More

Get Started

1
yarn add @payloadcms/plugin-form-builder
2
# OR
3
npm i @payloadcms/plugin-form-builder

Like what we're doing? Give us a star on GitHub

We're trying to change the CMS status quo by delivering editors with a great experience, but first and foremost, giving developers a CMS that they don't absolutely despise working with. All of our new features are meant to be extensible and work simply and sanely.