Skip to content

[6.x] Form Fields Refactor#14496

Open
duncanmcclean wants to merge 39 commits intoforms-2from
form-fields
Open

[6.x] Form Fields Refactor#14496
duncanmcclean wants to merge 39 commits intoforms-2from
form-fields

Conversation

@duncanmcclean
Copy link
Copy Markdown
Member

@duncanmcclean duncanmcclean commented Apr 15, 2026

This pull request refactors how form fields work under the hood, ahead of some changes we're making to forms.

TLDR: Form fields aren't stored in blueprints anymore, they're stored under a fields key in the form data. Fields will be migrated the next time the form is saved.

Problem

We want to offer different fieldtypes when editing forms than the ones available elsewhere.

A couple of examples:

  • We want to rename the Text/Textarea fieldtypes to "Short Answer" and "Long Answer", but just for forms.
  • We want to offer "Select" and "Multi-Select" fieldtypes, both of which use the Select fieldtype under the hood.
  • We want to provide separate fieldtypes for Name/Email/Phone, but they should all use the Text fieldtype under the hood.

Solution

Because they aren't exactly 1:1 mappings with existing fieldtypes, we need to create a layer in between the form builder and the underlying blueprint fields.

This layer (which we're referring to as "form fieldtypes") is responsible for wrapping normal fieldtypes. They can expose their own names, keywords, icons, config options, etc.

<?php

class ShortAnswer extends FormFieldtype
{
    protected static $handle = 'short_answer';
    protected static $icon = 'text';
    protected $categories = ['Generic Fields'];
    protected $keywords = ['text'];

    public function toFieldArray(): array
    {
        return [
            'type' => 'text',
        ];
    }
}

Then, instead of storing fields in a blueprint, like you would with normal fieldtypes, fields are stored in the form's data and map to one of the "form fieldtypes".

# resources/forms/contact_us.yaml
title: Contact us
fields:
  sections:
    -
      fields:
        -
          handle: name
          field:
            type: short_answer
            validate: required
        -
          handle: email
          field:
            type: email
            validate: required # already validates "email"
        -
          handle: message
          field:
            type: long_answer
            validate: required

Behind the scenes, there are new FormFields, FormField and FormFieldtype classes powering everything.

When rendering form fields — in the CP, on the frontend or in emails — the form fields will be converted to traditional blueprint fields.

Migrating blueprints

When the fields key is missing, Statamic will read from the form blueprint and convert fields to the new "form fieldtypes" on-the-fly.

For example: text fields will become short_answer fields, textarea fields will become long_answer fields, etc.

When there's no matching "form fieldtype" for an existing field, the Fallback form fieldtype will be used which literally returns the field config from the fieldtype's toFieldArray().

The fields key will be written to the form data array and the blueprint will be deleted the next time the form is saved.

Custom Form Fieldtypes

To build a custom form fieldtype, create a class that extends Statamic's FormFieldtype and add a toFieldArray() method.

<?php

namespace App\FormFieldtypes;

class Email extends FormFieldtype
{
    protected static $handle = 'email';
    protected static $icon = 'form-email';
    protected $categories = ['Contact Info'];
    protected $keywords = ['mailto', 'website'];

    public function configFieldItems(): array
    {
        return [
            'placeholder' => ['type' => 'text', 'display' => 'Placeholder'],
        ];
    }

    public function toFieldArray(): array
    {
        return [
            'type' => 'text',
            'validate' => 'email',
            'placeholder' => $this->config('placeholder'),
        ];
    }
}

The toFieldArray() method is responsible for building the underlying blueprint field used when rendering the field.

You may also add a configFieldItems() method to define any config options which will be editable in the UI.

In an app, simply put your class in the app/FormFieldtypes directory and it'll be registered automatically.

In an addon, put your class in the src/FormFieldtypes directory or register it manually in your ServiceProvider.php:

protected $formFieldtypes = [
	FormFieldtypes\Custom::class,
];

$selectableInForms & makeSelectableInForms()

The $selectableInForms property and the makeSelectableInForms() / makeUnselectableInForms() methods on the Fieldtype class have been deprecated and will be removed in v7.

Until then, fieldtypes using these will continue to show in the form builder — they'll be automatically wrapped in the fallback form fieldtype.

Going forward, you should create a FormFieldtype class instead. You can then use makeSelectable() or makeUnselectable() on the form fieldtype class directly:

GoogleMaps::makeSelectable();
GoogleMaps::makeUnselectable();

@duncanmcclean duncanmcclean marked this pull request as ready for review April 17, 2026 16:18
Comment thread src/Forms/Form.php
public function fields()
{
if (! $blueprint = $this->blueprint()) {
throw BlueprintUndefinedException::create($this);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this exception as I don't believe it's been thrown since the v3 alpha (specifically since #2092 made blueprint() return an empty blueprint if none existed).

I also can't find any references to it on GitHub, Discord or Slack so it's probably safe to remove.

Comment thread src/Forms/Form.php

protected $handle;
protected $title;
protected $blueprint;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've removed this property as the fluent getter/setter for it was removed in the v3 alpha days (in #2092) and nothing seems to be using it. Probably safe to remove.

{
public function __invoke($form)
{
// TOOD: Remove from this controller when wiring up the form builder.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the TODO suggests, I'm planning on removing this when I wire up the form builder. It's just here for now so we can verify all the select/unselect logic works.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@duncanmcclean maybe make this TODO so you can find it later? minor typo

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doh! Sorted the typo.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants