Upgrading from v2#

While the changelog (opens new window) is the most comprehensive list of changes, this guide provides high-level overview and organizes changes by category.

Field and Field Layouts#

Fields and Field Layouts have been completely revamped in Formie 3. Previously, Formie extended the base Field (opens new window) class from Craft for all fields, including Element fields like Assets and Entries. While convenient to make use of Craft's implementation for field functionality, it's proved more of a hindrance with limiting what Formie fields can do, and causing issues when Craft update their own field classes.

Similarly, Craft has the concept of a Field Layout, which is attached to an element (in our case, a Submission) and defines what content an element has. But these are heavily opinionated and tied to control-panel behaviour, and Formie forms are designed to be used on the front-end. They're also more complex with Pages/Rows/Fields, as opposed to Tabs/Fields in Craft.

Finally, there's performance. Formie fields were stored alongside Craft's own fields, and made use of Craft's field layouts. Having potentially hundreds of fields stored alongside Craft fields meant that Craft would initialise these fields on every request, when they don't need to. Alongside content table changes from Craft itself, this meant big changes were in order.

So — Formie now stores all Fields, Rows, Pages and Field Layouts in its own database tables, with it's own logic. Submissions now also make use of our own Field Layout.

We've strived to make things backward compatible, and deprecated existing behaviour to ease the migration. This should only affect you if:

  • You have created a custom field class
  • You have custom JavaScript validation for fields
  • You iterate through or access submission content
  • You call getCustomFields() in your templates for Submissions
  • You call getFieldLayout() in your templates for Submissions
  • You manually render form Pages or Fields

Twig Templates#

The following describes Twig template changes for dealing with fields. You no longer need to get the Field Layout for the element, instead just accessing Pages, Rows and Fields directly from the element.

Form Fields#

Fetching the fields for a Form can be done with form.getFields().

// Formie v2
{% for field in form.getCustomFields() %}
    {# ... #}

// Formie v3
{% for field in form.getFields() %}
    {# ... #}

Submission Fields#

Fetching submission content no longer requires you to get the Field Layout for the submission with submission.getFieldLayout().getCustomFields(). You can now just use submission.getFields().

// Formie v2
{% set fieldLayout = submission.getFieldLayout() %}

{% if fieldLayout %}
    {% for field in fieldLayout.getCustomFields() %}
        {# ... #}

// Formie v3
{% for field in submission.getFields() %}
    {# ... #}

Page and Row Fields#

Pages and Rows can use getFields() instead of getCustomFields().

// Formie v2
{% for page in form.getPages() %}
    {% for field in page.getCustomFields() %}
        {# ... #}

// Formie v3
{% for page in form.getPages() %}
    {% for field in page.getFields() %}
        {# ... #}

In addition, for rows, you should use row.getFields() as opposed to row.fields (although both will work).

// Formie v2
{% for page in form.getPages() %}
    {% for row in page.getRows() %}
        {% for field in row.fields %}
            {# ... #}

// Formie v3
{% for page in form.getPages() %}
    {% for row in page.getRows() %}
        {% for field in row.getFields() %}
            {# ... #}

Custom Field Classes#

If you haven't created your own custom field class for Formie, you can skip this section.

Because of the re-architecture of Formie fields, this may affect your custom field, depending on what it is. In Formie 2, we mixed classes and traits so that we could extend from the Craft base Field (opens new window) class. All traits have now been removed in favour of proper classes that you can extend from.

To outline some of the changes to Formie field classes (in case your field extends one), refer to the below table.

FieldSummary
AddressNow extends verbb\formie\base\SubField
CategoriesNow extends verbb\formie\base\ElementField, and no longer extends craft\fields\Categories
CheckboxesNow extends verbb\formie\base\OptionsField
DateNow extends verbb\formie\base\SubField
DropdownNow extends verbb\formie\base\OptionsField
EntriesNow extends verbb\formie\base\ElementField, and no longer extends craft\fields\Entries
FileUploadNow extends verbb\formie\base\ElementField, and no longer extends craft\fields\Assets
GroupNow extends verbb\formie\base\SingleNestedField
HeadingNow extends verbb\formie\base\CosmeticField
HtmlNow extends verbb\formie\base\CosmeticField
NameNow extends verbb\formie\base\SubField
PhoneNow extends verbb\formie\base\SubField
ProductsNow extends verbb\formie\base\ElementField, and no longer extends craft\commerce\fields\Products
RadioNow extends verbb\formie\base\OptionsField
RepeaterNow extends verbb\formie\base\MultiNestedField
SectionNow extends verbb\formie\base\CosmeticField
SummaryNow extends verbb\formie\base\CosmeticField
TableNo longer extends craft\fields\Table
TagsNow extends verbb\formie\base\ElementField, and no longer extends craft\fields\Tags
UsersNow extends verbb\formie\base\ElementField, and no longer extends craft\fields\Users
VariantsNow extends verbb\formie\base\ElementField, and no longer extends craft\commerce\fields\Variants

This shouldn't affect the vast majority of custom field classes, but it's a good reference in case you were extending one of the above.

Updated Class Namespace#

We've also cleaned up class namespaces for fields. While we have aliases setup for old classes to ease the migration, these will be removed in Formie 4. If your custom class contains any reference to these, they should be updated.

Old ClassNew Class
verbb\formie\base\FormFieldverbb\formie\base\Field
verbb\formie\base\FormFieldInterfaceverbb\formie\base\FieldInterface
verbb\formie\base\FormFieldTraitRemoved. Use verbb\formie\base\Field
verbb\formie\base\NestedFieldTraitRemoved. verbb\formie\base\NestedField
verbb\formie\base\RelationFieldTraitRemoved. verbb\formie\base\ElementField
verbb\formie\base\SubfieldInterfaceverbb\formie\base\SubFieldInterface
verbb\formie\base\SubfieldTraitRemoved. verbb\formie\base\SubField
verbb\formie\fields\formfields\Addressverbb\formie\fields\Address
verbb\formie\fields\formfields\Agreeverbb\formie\fields\Agree
verbb\formie\fields\formfields\Calculationsverbb\formie\fields\Calculations
verbb\formie\fields\formfields\Categoriesverbb\formie\fields\Categories
verbb\formie\fields\formfields\Checkboxesverbb\formie\fields\Checkboxes
verbb\formie\fields\formfields\Dateverbb\formie\fields\Date
verbb\formie\fields\formfields\Dropdownverbb\formie\fields\Dropdown
verbb\formie\fields\formfields\Emailverbb\formie\fields\Email
verbb\formie\fields\formfields\Entriesverbb\formie\fields\Entries
verbb\formie\fields\formfields\FileUploadverbb\formie\fields\FileUpload
verbb\formie\fields\formfields\Groupverbb\formie\fields\Group
verbb\formie\fields\formfields\Headingverbb\formie\fields\Heading
verbb\formie\fields\formfields\Hiddenverbb\formie\fields\Hidden
verbb\formie\fields\formfields\Htmlverbb\formie\fields\Html
verbb\formie\fields\formfields\MissingFieldverbb\formie\fields\MissingField
verbb\formie\fields\formfields\MultiLineTextverbb\formie\fields\MultiLineText
verbb\formie\fields\formfields\Nameverbb\formie\fields\Name
verbb\formie\fields\formfields\Numberverbb\formie\fields\Number
verbb\formie\fields\formfields\Passwordverbb\formie\fields\Password
verbb\formie\fields\formfields\Paymentverbb\formie\fields\Payment
verbb\formie\fields\formfields\Phoneverbb\formie\fields\Phone
verbb\formie\fields\formfields\Productsverbb\formie\fields\Products
verbb\formie\fields\formfields\Radioverbb\formie\fields\Radio
verbb\formie\fields\formfields\Recipientsverbb\formie\fields\Recipients
verbb\formie\fields\formfields\Repeaterverbb\formie\fields\Repeater
verbb\formie\fields\formfields\Sectionverbb\formie\fields\Section
verbb\formie\fields\formfields\Signatureverbb\formie\fields\Signature
verbb\formie\fields\formfields\SingleLineTextverbb\formie\fields\SingleLineText
verbb\formie\fields\formfields\Summaryverbb\formie\fields\Summary
verbb\formie\fields\formfields\Tableverbb\formie\fields\Table
verbb\formie\fields\formfields\Tagsverbb\formie\fields\Tags
verbb\formie\fields\formfields\Usersverbb\formie\fields\Users
verbb\formie\fields\formfields\Variantsverbb\formie\fields\Variants

Changed Methods#

We have also changed several methods.

OldWhat to do instead
Formie::log()Formie::info()
Field::hasSubfields()Field::hasSubFields()
Field::getBaseFieldConfig()Field::getFieldTypeConfig()
Field::getExtraBaseFieldConfig()Field::getFieldTypeConfigData()
Field::getSavedFieldConfig()Field::getFormBuilderConfig()
Field::getFieldDefaults()Field::getFieldTypeConfigDefaults()
Field::getAllFieldDefaults()Field::getFieldTypeConfigDefaults()
Field::getFieldValue()$element->getFieldValue($field->handle)
Field::getContentGqlMutationArgument()Field::getContentGqlMutationArgumentType()

Method Signatures#

To adhere to more strict typing introduced in PHP 8, we've modified some method signatures. Your custom field classes will need to be updated to reflect these changes.

// Formie v2 vs Formie v3
public function getInputHtml($value, ElementInterface $element = null): string
protected function cpInputHtml(mixed $value, ?ElementInterface $element, bool $inline): string

// Formie v2 vs Formie v3
public function normalizeValue(mixed $value, ?ElementInterface $element = null): mixed
public function normalizeValue(mixed $value, ElementInterface $element = null): mixed

// Formie v2 vs Formie v3
public function getValueForIntegration(mixed $value, $integrationField, $integration, ?ElementInterface $element = null, $fieldKey = ''): mixed
public function getValueForIntegration(mixed $value, IntegrationField $integrationField, IntegrationInterface $integration, ?ElementInterface $element = null, string $fieldKey = ''): mixed

// Formie v2 vs Formie v3
public function getValueForEmail(mixed $value, $notification, ?ElementInterface $element = null): mixed
public function getValueForEmail(mixed $value, Notification $notification, ?ElementInterface $element = null): mixed

// Formie v2 vs Formie v3
public function populateValue($value): void
public function populateValue(mixed $value): void

// Formie v2 vs Formie v3
public function setForm($value): void
public function setForm(?Form $value): void

// Formie v2 vs Formie v3
public function getDefaultValue($attributePrefix = '')
public function getDefaultValue(): mixed

// Formie v2 vs Formie v3
public function setNamespace($value): void
public function setNamespace(string|bool|null $value): void

// Formie v2 vs Formie v3
public function getConditionsJson($element = null): ?string
public function getConditionsJson(): ?string

// Formie v2 vs Formie v3
public function isConditionallyHidden(Submission $element): bool
public function isConditionallyHidden(Submission $submission): bool

// Formie v2 vs Formie v3
protected function setPrePopulatedValue($value)
protected function setPrePopulatedValue(mixed $value): mixed

// Formie v2 vs Formie v3
protected function defineValueAsString($value, ElementInterface $element = null): string
protected function defineValueAsString(mixed $value, ElementInterface $element = null): string

// Formie v2 vs Formie v3
protected function defineValueAsJson($value, ElementInterface $element = null): mixed
protected function defineValueAsJson(mixed $value, ElementInterface $element = null): mixed

// Formie v2 vs Formie v3
protected function defineValueForExport($value, ElementInterface $element = null): mixed
protected function defineValueForExport(mixed $value, ElementInterface $element = null): mixed

// Formie v2 vs Formie v3
protected function defineValueForIntegration($value, $integrationField, $integration, ElementInterface $element = null, $fieldKey = ''): mixed
protected function defineValueForIntegration(mixed $value, IntegrationField $integrationField, IntegrationInterface $integration, ElementInterface $element = null, string $fieldKey = ''): mixed

// Formie v2 vs Formie v3
protected function defineValueForSummary($value, ElementInterface $element = null): string
protected function defineValueForSummary(mixed $value, ElementInterface $element = null): string

// Formie v2 vs Formie v3
protected function defineValueForEmail($value, $notification, ElementInterface $element = null): string
protected function defineValueForEmail(mixed $value, Notification $notification, ElementInterface $element = null): string

// Formie v2 vs Formie v3
protected function getContentGqlMutationArgument(): Type|array|null
protected function getContentGqlMutationArgumentType(): Type|array

Custom JavaScript Validation#

If you haven't created your own custom JavaScript validation handlers, you can skip this section.

Client-side validation has been greatly improved, and now aligns more closely to server-side validation from Craft/Yii. Previously, we used bouncer.js to handle client-side validation, which while excellent wasn't really translating well to some opinionated Formie concepts, or being used to compliment Craft/Yii's server-side validation.

In fact, we used bouncer.js as inspiration for our own validation library.

This breaking change affects how you register your custom validators.

// Formie v2
import { t } from 'vendor/formie/frontend/utils/utils';

let $form = document.querySelector('#formie-form-1');

function customRule() {
    return {
        minLength(field) {
            const limit = field.getAttribute('data-limit');

            // Don't trigger this validation unless there's the `data-limit` attribute
            if (!limit) {
                return false;
            }

            return !(field.value.length > limit);
        },
    };
}

function customMessage() {
    return {
        minLength(field) {
            return t('The value entered must be at least {limit} characters long.', {
                limit: field.getAttribute('data-limit'),
            });
        },
    };
}

$form.addEventListener('registerFormieValidation', (e) => {
    e.preventDefault();

    // Add our custom validations logic and methods
    e.detail.validatorSettings.customValidations = {
        ...e.detail.validatorSettings.customValidations,
        ...this.customRule(),
    };

    // Add our custom messages
    e.detail.validatorSettings.messages = {
        ...e.detail.validatorSettings.messages,
        ...this.customMessage(),
    };

    // ...
});

// Formie v3
let $form = document.querySelector('#formie-form-1');

$form.addEventListener('onFormieThemeReady', (event) => {
    event.detail.addValidator('minLength', ({ input }) => {
        const limit = input.getAttribute('data-limit');

        // Don't trigger this validation unless there's the `data-limit` attribute
        if (!limit) {
            return true;
        }

        return input.value.length > limit;
    }, ({ label, input, t }) => {
        const limit = input.getAttribute('data-limit');

        return t('The value entered in {label} must be at least {limit} characters long.', { label, limit });
    });
});

Now, we use a single registration function addValidator() to register our custom validator and message. This is in contrast to separate registration functions for the validation logic and message. We also have access to more variables in the respective callbacks for the field (Formie's outer field wrapper) the input (the <input> or similar HTML element) and the label of the field.

We can also make use of Craft's translations far easier with the t() function available. You could also use string interpolation as well (i.e. ${label} is invalid.) along with accessing other aspects of the DOM.

We also use the new onFormieThemeReady event to register these validations only if you're using Formie's Theme JS (where client-side validation is enabled).

Return Value#

The returned value is now the opposite of what it was. Previously, a true return was used if the validation were to be applied. This was somewhat confusing, as returning true from a cognitive point of view usually means a positive change. Returning true for something that meant a failure didn't seem to make sense.

As such, validators should now return true for passing and false for failing. Essentially, describing whether the field value passed validation or not.

// Formie v3
function customRule() {
    return {
        minLength(field) {
            // Don't trigger this validation unless there's the `data-limit` attribute
            if (!field.getAttribute('data-limit')) {
                return false;
            }

            return !(field.value.length > 5);
        },
    };
}

// Formie v3
function({ input }) {
    // Don't trigger this validation unless there's the `data-limit` attribute
    if (!input.getAttribute('data-limit')) {
        return true;
    }

    return input.value.length > 5;
}

Repeater & Group Fields#

Repeater and Group fields have had a major overhaul in Formie 3, most notably that their values are no longer elements. This is to simplify the field, provide a lower learning curve to dealing with them, and performance. They also don't use nested field layouts for the same goals. Instead, they are simple arrays.

This should only effect users who customise behaviour of these fields, or when working with the values of these fields. However, it should be much more simplified to do so.

// Formie v2
{# Fields would return a `verbb\formie\elements\db\NestedBlockQuery` object #}
{% set groupValue = myGroupField.one() %}
{{ groupValue.innerField1 }}
{{ groupValue.innerField2 }}

{# Fields would return a `verbb\formie\elements\db\NestedBlockQuery` object #}
{% set repeaterValue = myRepeaterField.all() %}

{% for row in repeaterValue %}
    {{ row.innerField1 }}
    {{ row.innerField2 }}
{% endfor %}

// Formie v3
{# Fields are now an array #}
{{ myGroupField.innerField1 }}
{{ myGroupField.innerField2 }}

{# Fields are now an array #}
{% for row in myRepeaterField %}
    {{ row.innerField1 }}
    {{ row.innerField2 }}
{% endfor %}

In addition, you can also now access field value directly from the submission, with dot-notation syntax.

{{ submission.getFieldValue('multiName.firstName') }}
{{ submission.getFieldValue('myGroupField.innerField1') }}
{{ submission.getFieldValue('myRepeaterField.0.innerField1') }}
{{ submission.getFieldValue('myRepeaterField.1.innerField1') }}

The latter example of a Repeater includes the zero-based index of the row you're fetching values for.

Sub-Fields#

Sub-Fields have also received a revamp in their implementation. We wanted to provide the means to manage more advanced settings for sub-fields, along with being able to re-order them.

Sub-Fields feature a new user interface, similar to a Group field. Sub-fields can be enabled/disabled, re-ordered and can have all their settings managed like regular fields. Sub-fields cannot be removed from their parent field, only disabled - which is the main difference between achieving a similar thing with a Group field.

Additionally, sub-fields are now stored in the formie_fields database table as fields within their own layout (again, the same as a Group or Repeater field).

We also use a new "type" for sub-fields under the verbb\formie\fields\subfields namespace. This allows us to extend existing fields to provide additional functionality specific to that fields' use-case. If you're creating your own sub-fields, this is completely optional, and any Formie field will be able to be used as a sub-field. For example, an Address Country sub-field extends a Dropdown field, but provides an easier way to manage the number of options to automatically generate based on a list of countries, rather than managing them yourself.

Current Sub-Fields are Name (multi-field enabled), Address, and Date (dropdown and input layouts). Phone fields were previously a sub-field, but are now just a regular field.

Under the hood, a verbb\formie\base\SubField extends the verbb\formie\base\SingleNestedField class, which in turn extends verbb\formie\base\NestedField. The SingleNestedField is the same class that a Group field uses, as the two types of fields are almost identical.

All sub-fields are migrated over to this new implementation. If you have custom sub-field's, you'll need to review your implementation to follow the updated behaviour.

As field settings now exist in their respective sub-field classes and fields, we have removed many properties.

Address Fields#

  • autocompleteIntegration
  • autocompleteEnabled
  • autocompleteCollapsed
  • autocompleteLabel
  • autocompletePlaceholder
  • autocompleteDefaultValue
  • autocompletePrePopulate
  • autocompleteRequired
  • autocompleteErrorMessage
  • autocompleteCurrentLocation
  • address1Enabled
  • address1Collapsed
  • address1Label
  • address1Placeholder
  • address1DefaultValue
  • address1PrePopulate
  • address1Required
  • address1ErrorMessage
  • address1Hidden
  • address2Enabled
  • address2Collapsed
  • address2Label
  • address2Placeholder
  • address2DefaultValue
  • address2PrePopulate
  • address2Required
  • address2ErrorMessage
  • address2Hidden
  • address3Enabled
  • address3Collapsed
  • address3Label
  • address3Placeholder
  • address3DefaultValue
  • address3PrePopulate
  • address3Required
  • address3ErrorMessage
  • address3Hidden
  • cityEnabled
  • cityCollapsed
  • cityLabel
  • cityPlaceholder
  • cityDefaultValue
  • cityPrePopulate
  • cityRequired
  • cityErrorMessage
  • cityHidden
  • stateEnabled
  • stateCollapsed
  • stateLabel
  • statePlaceholder
  • stateDefaultValue
  • statePrePopulate
  • stateRequired
  • stateErrorMessage
  • stateHidden
  • zipEnabled
  • zipCollapsed
  • zipLabel
  • zipPlaceholder
  • zipDefaultValue
  • zipPrePopulate
  • zipRequired
  • zipErrorMessage
  • zipHidden
  • countryEnabled
  • countryCollapsed
  • countryLabel
  • countryPlaceholder
  • countryDefaultValue
  • countryPrePopulate
  • countryRequired
  • countryErrorMessage
  • countryHidden
  • countryOptionLabel
  • countryOptionValue

Date Fields#

  • dayLabel
  • dayPlaceholder
  • monthLabel
  • monthPlaceholder
  • yearLabel
  • yearPlaceholder
  • hourLabel
  • hourPlaceholder
  • minuteLabel
  • minutePlaceholder
  • secondLabel
  • secondPlaceholder
  • ampmLabel
  • ampmPlaceholder
  • minYearRange
  • maxYearRange
  • timeLabel
  • includeDate
  • includeTime

Name Fields#

  • prefixEnabled
  • prefixCollapsed
  • prefixEnabled
  • prefixCollapsed
  • prefixLabel
  • prefixPlaceholder
  • prefixDefaultValue
  • prefixPrePopulate
  • prefixRequired
  • prefixErrorMessage
  • firstNameEnabled
  • firstNameCollapsed
  • firstNameLabel
  • firstNamePlaceholder
  • firstNameDefaultValue
  • firstNamePrePopulate
  • firstNameRequired
  • firstNameErrorMessage
  • middleNameEnabled
  • middleNameCollapsed
  • middleNameLabel
  • middleNamePlaceholder
  • middleNameDefaultValue
  • middleNamePrePopulate
  • middleNameRequired
  • middleNameErrorMessage
  • lastNameEnabled
  • lastNameCollapsed
  • lastNameLabel
  • lastNamePlaceholder
  • lastNameDefaultValue
  • lastNamePrePopulate
  • lastNameRequired
  • lastNameErrorMessage

Phone Fields#

  • countryCollapsed
  • countryShowDialCode

Example#

If you wish to access the sub-field settings, you'll need to access the sub-field first. For example, if you were in the context of a field class:

// Formie v2
$firstNameEnabled = $this->firstNameEnabled;

if ($firstNameEnabled) {
    $label = $this->firstNameLabel;
}

// Formie v3
$firstName = $this->getFieldByHandle('firstName');

if ($firstName && $firstName->enabled) {
    $label = $firstName->label;
}

Content Tables#

Inline with Craft's own improvement of removing the content database table and the about Field/Field Layout changes, Formie now no longer stores submission content in a fmc_* database table. Instead, it's a single content JSON column in your formie_submissions database table. This not only has performance benefits, but makes it easy to view your content directly from the database.

Importantly, this change also means that you can now have unlimited fields in a form, so if you've been hanging out to make the biggest web form in history - your time is now!

This won't mean anything for day-to-day use of Formie, but is useful to know if you're familiar with looking at the raw content of a submission.

Theme Config#

The fieldInputContainer key for Theme Config has been renamed to fieldInputWrapper to follow clear consistency with the terms "wrapper" and "container".

In addition, the .fui-input-container class now no longer exists, replaced with .fui-input-wrapper, so any CSS overrides you have will need to be updated.

Integrations#

If you haven't created your own custom integration class for Formie, you can skip this section.

OAuth & Auth Module#

To simplify and streamline integration logic for OAuth-based providers, we're using the Auth Module to handle the authentication flow and token handling. For non-OAuth-based providers, there will be no change.

While things should be seamlessly migrated from Formie to Auth, if you have custom integrations that use OAuth, you'll need to integrate those with the Auth module. Formie no longer contains many of the helper methods that an older OAuth integration would've relied on.

In addition, if you were using any of the OAuth-specific functions in Formie, those are now removed.

The following classes have been removed

  • verbb\formie\events\OauthTokenEvent
  • verbb\formie\events\TokenEvent
  • verbb\formie\models\Token
  • verbb\formie\records\Token
  • verbb\formie\services\Tokens

The formie_tokens database table has been removed and items migrated to the auth_oauth_tokens database table.

For the Integration class, the following applies:

  • Integration::tokenId property removed
  • Integration::getAuthorizeUrl() method removed
  • Integration::getAccessTokenUrl() method removed
  • Integration::getResourceOwner() method removed
  • Integration::getOauthScope() method removed
  • Integration::getOauthAuthorizationOptions() method removed
  • Integration::oauthVersion() method removed
  • Integration::oauth2Legged() method removed
  • Integration::oauthConnect() method removed
  • Integration::getOauthProviderConfig() method removed
  • Integration::getOauthProvider() method removed
  • Integration::beforeFetchAccessToken() method removed
  • Integration::afterFetchAccessToken() method removed

Custom Integration classes should now implement a getOAuthProviderClass() method which is a Auth Provider.

Refer also to our OAuth Integrations (opens new window) for some existing implementations. We also have an example (opens new window) integration to get started with.

GraphQL#

When querying fields or pages, you should use the label property instead of name.

// Formie v2
{
    formieForm (handle: "contactForm") {
        title
        
        pages {
            name

            rows {
                rowFields {
                    name
                }
            }
        }
    }
}

// Formie v3
{
    formieForm (handle: "contactForm") {
        title
        
        pages {
            label

            rows {
                rowFields {
                    label
                }
            }
        }
    }
}

Sub-Fields#

Previously, all Sub-Field label, enabled state and more were stored as properties on the outer Sub-Field. These are now nested as individual FieldInterface items.

// Formie v2
{
    formieForm (handle: "contactForm") {
        title
        
        formFields {
            label

            prefixLabel
            firstNameLabel
            middleNameLabel
            lastNameLabel
        }
    }
}

// Formie v3
{
    formieForm (handle: "contactForm") {
        title
        
        formFields {
            label

            ... on Field_Name {
                fields {
                    label
                }
            }
        }
    }
}

Events#

The EVENT_MODIFY_FIELD_CONFIG event had been moved to be on the individual field class, not the verbb\formie\services\Fields service.

// Formie v2
use verbb\formie\events\ModifyFieldConfigEvent;
use verbb\formie\services\Fields;
use yii\base\Event;

Event::on(Fields::class, Fields::EVENT_MODIFY_FIELD_CONFIG, function(ModifyFieldConfigEvent $event) {
    $config = $event->config;
    // ...
});

// Formie v3
use verbb\formie\events\ModifyFieldConfigEvent;
use verbb\formie\fields\formfields\SingleLineText;
use yii\base\Event;

Event::on(SingleLineText::class, SingleLineText::EVENT_MODIFY_FIELD_CONFIG, function(ModifyFieldConfigEvent $event) {
    $config = $event->config;
    // ...
});

Address modifyFrontEndSubfields event#

The EVENT_MODIFY_FRONT_END_SUBFIELDS event had been renamed to EVENT_MODIFY_NESTED_FIELD_LAYOUT and differs slightly. Instead of modifying the config of a field as an array, before it's created as a field, you can now modify the created field in the context of a field layout.

// Formie v2
use craft\helpers\ArrayHelper;
use verbb\formie\events\ModifyFrontEndSubfieldsEvent;
use verbb\formie\fields\formfields\Address;
use yii\base\Event;

Event::on(Address::class, Address::EVENT_MODIFY_FRONT_END_SUBFIELDS, function(ModifyFrontEndSubfieldsEvent $event) {
    $address1 = $event->rows[0][0];

    // Modify the `address1` field - a `SingleLineText` field.
    $address1->name = 'Address Line 1';

    $event->rows[0][0] = $address1;

    // Change the order of fields - remove them first
    $address2 = ArrayHelper::remove($event->rows[1], 0);
    $address3 = ArrayHelper::remove($event->rows[2], 0);

    // Add them to the first row
    $event->rows[0][] = $address2;
    $event->rows[0][] = $address3;
});

// Formie v3
use verbb\formie\events\ModifyNestedFieldLayoutEvent;
use verbb\formie\fields\Address;
use yii\base\Event;

Event::on(Address::class, Address::EVENT_MODIFY_NESTED_FIELD_LAYOUT, function(ModifyNestedFieldLayoutEvent $event) {
    // Lookup the last name sub-field. We can no longer rely on a static order
    $address1 = $event->fieldLayout->getFieldByHandle('address1');

    // Modify the `address1` field - a `SingleLineText` field.
    $address1->label = 'Address Line 1';
});

Date modifyFrontEndSubfields event#

The EVENT_MODIFY_FRONT_END_SUBFIELDS event had been renamed to EVENT_MODIFY_NESTED_FIELD_LAYOUT and differs slightly. Instead of modifying the config of a field as an array, before it's created as a field, you can now modify the created field in the context of a field layout.

// Formie v2
use verbb\formie\events\ModifyFrontEndSubfieldsEvent;
use verbb\formie\fields\formfields\Date;
use yii\base\Event;
use DateTime;

Event::on(Date::class, Date::EVENT_MODIFY_FRONT_END_SUBFIELDS, function(ModifyFrontEndSubfieldsEvent $event) {
    $field = $event->field;

    $defaultValue = $field->defaultValue ? $field->defaultValue : new DateTime();
    $year = intval($defaultValue->format('Y'));
    $minYear = $year - 10;
    $maxYear = $year + 10;

    $yearOptions = [];

    for ($y = $minYear; $y < $maxYear; ++$y) {
        $yearOptions[] = ['value' => $y, 'label' => $y];
    }

    $event->rows[0]['Y'] = [
        'handle' => 'year',
        'options' => $yearOptions,
        'min' => $minYear,
        'max' => $maxYear,
    ];
});

// Formie v3
use verbb\formie\events\ModifyNestedFieldLayoutEvent;
use verbb\formie\fields\Date;
use yii\base\Event;

Event::on(Date::class, Date::EVENT_MODIFY_NESTED_FIELD_LAYOUT, function(ModifyNestedFieldLayoutEvent $event) {
    // Lookup the last name sub-field. We can no longer rely on a static order
    $year = ArrayHelper::firstWhere($event->fields, 'handle', 'year');

    $yearOptions = [];
    $minYear = 2000;
    $maxYear = 2050;

    for ($y = $minYear; $y < $maxYear; ++$y) {
        $yearOptions[] = ['value' => $y, 'label' => $y];
    }

    // Modify the `year` field - a `SingleLineText` field.
    $year->options = $yearOptions;
    $year->min = $minYear;
    $year->max = $maxYear;
});

Name modifyFrontEndSubfields event#

The EVENT_MODIFY_FRONT_END_SUBFIELDS event had been renamed to EVENT_MODIFY_NESTED_FIELD_LAYOUT and differs slightly. Instead of modifying the config of a field as an array, before it's created as a field, you can now modify the created field in the context of a field layout.

// Formie v2
use verbb\formie\events\ModifyFrontEndSubfieldsEvent;
use verbb\formie\fields\formfields\Name;
use yii\base\Event;

Event::on(Name::class, Name::EVENT_MODIFY_FRONT_END_SUBFIELDS, function(ModifyFrontEndSubfieldsEvent $event) {
    $lastName = $event->rows[0][3];

    // Modify the `lastName` field - a `SingleLineText` field.
    $lastName->name = 'Surname';

    $event->rows[0][3] = $lastName;

    // Change the order of fields - remove them first
    $firstName = ArrayHelper::remove($event->rows[0], 1);
    $lastName = ArrayHelper::remove($event->rows[0], 3);

    // Reverse the order
    $event->rows[0][1] = $lastName;
    $event->rows[0]3 = $firstName;
});

// Formie v3
use verbb\formie\events\ModifyNestedFieldLayoutEvent;
use verbb\formie\fields\Name;
use yii\base\Event;

Event::on(Name::class, Name::EVENT_MODIFY_NESTED_FIELD_LAYOUT, function(ModifyNestedFieldLayoutEvent $event) {
    // Lookup the last name sub-field. We can no longer rely on a static order
    $lastName = $event->fieldLayout->getFieldByHandle('lastName');

    // Modify the `lastName` field - a `SingleLineText` field.
    $lastName->label = 'Surname';
});

Name modifyPrefixOptions event#

The EVENT_MODIFY_PREFIX_OPTIONS has been moved to the inner Prefix sub-field class.

// Formie v2
use verbb\formie\events\ModifyNamePrefixOptionsEvent;
use verbb\formie\fields\Name;
use yii\base\Event;

Event::on(Name::class, Name::EVENT_MODIFY_PREFIX_OPTIONS, function(ModifyNamePrefixOptionsEvent $event) {
    $event->options[] = ['label' => Craft::t('formie', 'Mx.'), 'value' => 'mx'];
});

// Formie v3
use verbb\formie\events\ModifyNamePrefixOptionsEvent;
use verbb\formie\fields\subfields\NamePrefix;
use yii\base\Event;

Event::on(NamePrefix::class, NamePrefix::EVENT_MODIFY_PREFIX_OPTIONS, function(ModifyNamePrefixOptionsEvent $event) {
    $event->options[] = ['label' => Craft::t('formie', 'Mx.'), 'value' => 'mx'];
});

Previous ← Troubleshooting Next Forms →