npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@foundrykit/advanced-forms-plugin

v1.5.1

Published

Multi-step enquiry form plugin for Payload CMS 3

Downloads

668

Readme

@foundrykit/advanced-forms-plugin

A multi-step form plugin for Payload CMS 3. Adds a form builder to your admin panel with 11 built-in field types, configurable submission actions, and a ready-to-use React component for the frontend.

Features

  • Multi-step forms — organise fields into named steps with optional icons and a progress indicator
  • 11 field types — text, email, phone, textarea, checkbox, radio group, checkbox group, select, number, date, file upload
  • Submission actions — send email, show a confirmation message, or redirect after submission (multiple actions per form, executed in order)
  • Two collectionsforms and form-submissions added to your Payload config automatically
  • REST API — fetch form data and submit forms via Payload custom endpoints
  • Frontend component<EnquiryForm> React client component with built-in step navigation and validation
  • Fully customisable — disable, replace, or add field block types; rename collection slugs

Installation

pnpm add @foundrykit/advanced-forms-plugin

Peer dependencies (install if not already present):

pnpm add payload react react-hook-form @radix-ui/react-popover

Quick start

// payload.config.ts
import { formPlugin } from '@foundrykit/advanced-forms-plugin'
import { buildConfig } from 'payload'

export default buildConfig({
  // Configure your email adapter here — the plugin uses payload.sendEmail
  // for the `sendEmail` submission action.
  // email: nodemailerAdapter({ ... }) or resendAdapter({ ... }), etc.
  plugins: [formPlugin({})],
  // ...rest of your config
})

The sendEmail submission action uses Payload's built-in payload.sendEmail, which delegates to the email adapter you configure on the Payload config. No plugin-level email handler is required.


Plugin options

formPlugin(options: FormPluginConfig)

| Option | Type | Default | Description | |---|---|---|---| | disabled | boolean | false | Register collections but skip adding endpoints | | baseUrl | string | '' | Base URL prepended when building form API URLs (e.g. https://example.com) | | mediaCollection | string | 'media' | Slug of your media collection, used for step icon uploads | | collections.forms | string | 'forms' | Slug for the forms collection | | collections.submissions | string | 'form-submissions' | Slug for the submissions collection | | labels.forms | string | 'Form' | Singular admin label for forms | | labels.submissions | string | 'Form Submission' | Singular admin label for submissions | | fields | FieldsConfig | all built-ins | Control which field block types are available in the form builder — see Customising field blocks |


Collections

Forms (forms)

Stores form definitions. Publicly readable; create/update/delete require authentication.

Each form document has:

  • title — display name
  • slug — unique identifier used in the API (e.g. contact-form)
  • steps — one or more steps, each with a title, optional icon, and a list of field blocks
  • additionalContent — optional rich text positioned above or below the form
  • submissionActions — one or more actions executed on successful submission

Form Submissions (form-submissions)

One document per submission. Stores:

  • form — relationship to the parent form
  • submittedAt — ISO timestamp
  • data — key/value pairs of submitted field values
  • metadatauserAgent, ip, referrer captured from the request

Field types

| Block slug | Description | |---|---| | text | Single-line text input (supports half width) | | email | Email address input | | phone | Phone number input | | textarea | Multi-line text (configurable rows) | | checkbox | Single boolean checkbox | | radioGroup | Single choice from a list (row or grid layout) | | checkboxGroup | Multiple choices from a list (row or grid layout) | | select | Dropdown select | | number | Numeric input with optional min, max, step | | date | Date picker with optional min/max | | file | File upload to a Payload collection (configurable accept, maxSizeMB) |

All field blocks share these base properties: name, label, required, tooltip.


Submission actions

Configure one or more actions per form. They execute in the order they appear.

| Block | Description | |---|---| | sendEmail | Sends an email via payload.sendEmail (uses your Payload email adapter). Subject supports {{fieldName}} interpolation. | | confirmationMessage | Returns a Lexical rich-text message in the API response for the frontend to display. | | redirect | Returns a redirectUrl in the API response. The frontend component handles the navigation. |


REST API

Both endpoints are registered at the Payload API root (/api by default).

GET /api/form-data/:slug

Fetches a form document by its slug.

Response: the full FormDocument JSON.

POST /api/form-submit/:formSlug

Submits field data for a form.

Request body:

{
  "data": { "name": "Alice", "email": "[email protected]" },
  "metadata": { "referrer": "https://example.com/about" }
}

Success response (200):

{
  "success": true,
  "actions": {
    "confirmationMessage": { /* Lexical rich-text node */ },
    "redirectUrl": "/thank-you"
  }
}

Validation error response (422):

{
  "success": false,
  "errors": [{ "field": "email", "message": "Email is required" }]
}

Frontend usage

Server component — fetch form data

Import from @foundrykit/advanced-forms-plugin/rsc:

import { fetchForm } from '@foundrykit/advanced-forms-plugin/rsc'

export default async function ContactPage() {
  const form = await fetchForm({ slug: 'contact-form' })
  return <ContactForm form={form} />
}

fetchForm options:

| Option | Type | Default | Description | |---|---|---|---| | slug | string | required | The form slug to fetch | | baseUrl | string | '' | Override the API base URL | | formsSlug | string | 'forms' | Override the forms collection slug used in the URL path |

Client component — render the form

Import from @foundrykit/advanced-forms-plugin/client:

'use client'

import { EnquiryForm } from '@foundrykit/advanced-forms-plugin/client'
import type { FormDocument } from '@foundrykit/advanced-forms-plugin'

export function ContactForm({ form }: { form: FormDocument }) {
  return (
    <EnquiryForm
      form={form}
      onSuccess={(result) => console.log('submitted', result)}
      onError={(err) => console.error(err)}
    />
  )
}

EnquiryForm props:

| Prop | Type | Default | Description | |---|---|---|---| | form | FormDocument | required | Form data returned by fetchForm | | apiBase | string | '' | Base URL for the submit endpoint | | className | string | — | CSS class applied to the root element | | onSuccess | (result: SubmitResult) => void | — | Called after successful submission | | onError | (error: SubmitError) => void | — | Called when the server returns validation errors | | additionalContent | ReactNode | — | Extra content rendered above or below the form (respects the additionalContent.position setting) | | resolver | Resolver<Record<string, unknown>> | — | Optional react-hook-form resolver (e.g. zodResolver(schema)). Merged with the per-field admin rules. |

Validation

Every field block exposes an admin Validation group: requiredMessage, minLength / maxLength / pattern / patternMessage (for text-shaped fields), and min / max / minMessage / maxMessage (for numeric fields and the multi-counter total). These rules are wired into RHF via the buildFieldRules helper and are also exported from /client for use in custom field components.

For richer cross-field logic, pass a form-level resolver. The plugin does not bundle zod or @hookform/resolvers — install them in your app:

pnpm add zod @hookform/resolvers
'use client'

import { EnquiryForm } from '@foundrykit/advanced-forms-plugin/client'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z
  .object({
    travelKnown: z.enum(['yes', 'no']).optional(),
    travelPlans: z.string().optional(),
  })
  .passthrough()
  .superRefine((data, ctx) => {
    if (data.travelKnown === 'no' && !data.travelPlans) {
      ctx.addIssue({
        path: ['travelPlans'],
        code: 'custom',
        message: 'Tell us a bit about what you’re looking for',
      })
    }
  })

export function ContactForm({ form }) {
  return <EnquiryForm form={form} resolver={zodResolver(schema)} />
}

Per-field admin rules and the resolver run together — either source can flag a field invalid, and step-by-step validation (goNext) respects both.

Hook — custom form UI

If you need full control over the form UI, use useEnquiryForm directly:

'use client'

import { useEnquiryForm } from '@foundrykit/advanced-forms-plugin/client'

export function MyCustomForm({ form }) {
  const { currentStep, totalSteps, stepData, form: rhf, goNext, goBack, submit, isSubmitting } =
    useEnquiryForm({ form, apiBase: '' })

  // render whatever you like
}

Customising field blocks

The fields option in the plugin config controls which block types appear in the form builder:

formPlugin({
  fields: {
    // disable a built-in type
    file: false,

    // replace a built-in type with your own block
    text: { block: myCustomTextBlock },

    // add a new type
    budgetRange: { block: BudgetRangeBlock },
  },
})

Built-in slugs: text, email, phone, textarea, checkbox, radioGroup, checkboxGroup, select, number, date, file.


Architecture overview

src/
├── index.ts                  # Plugin entry — merges collections and endpoints into Payload config
├── types.ts                  # All shared TypeScript types (field blocks, FormDocument, plugin config)
├── collections/
│   ├── Forms.ts              # createFormsCollection factory
│   └── Submissions.ts        # createSubmissionsCollection factory
├── blocks/
│   ├── fields/               # One file per built-in field block type
│   └── submissionActions/    # SendEmail, ConfirmationMessage, Redirect blocks
├── endpoints/
│   ├── fetchFormHandler.ts   # GET /api/form-data/:slug
│   └── submitFormHandler.ts  # POST /api/form-submit/:formSlug — validates, stores, fires actions
├── components/
│   └── EnquiryForm/          # React client component and useEnquiryForm hook
├── utilities/
│   ├── fetchEnquiryForm.ts   # fetchForm — RSC utility wrapping the fetch endpoint
│   ├── buildFormURL.ts       # URL builder for the form-data endpoint
│   └── sanitizeSubmission.ts # Sanitises raw form data before storage
└── exports/
    ├── client.ts             # @foundrykit/advanced-forms-plugin/client exports
    └── rsc.ts                # @foundrykit/advanced-forms-plugin/rsc exports

The plugin follows the standard Payload plugin pattern: formPlugin(options) returns a function that receives and returns the Payload Config object. Collections and endpoints are added using spread syntax to avoid overwriting existing config.


Development

pnpm dev        # Start the dev app at http://localhost:3000
pnpm test       # Run unit + integration + e2e tests
pnpm build      # Compile to dist/

Copy dev/.env.example to dev/.env and set DATABASE_URL and PAYLOAD_SECRET before running dev.