Skip to content

Changelog - Form Builder

All notable changes to Form Builder are documented in this file.

The format is based on Keep a Changelog.

1.0.1 - 2026-10-13

This release gives the Form Builder settings section its own permission.

Noteworthy change Custom roles that manage the Form Builder settings need the new Hyvä Form Builder Section resource granted after upgrading.

Changed

  • The Form Builder settings section (Stores > Configuration > Hyvä Commerce > Form Builder) is gated by its own resource, Hyvä Form Builder Section (Hyva_FormBuilder::config) under Stores > Settings > Configuration, instead of Magento_Backend::content. Existing roles are not migrated.

1.0.0 - 2026-10-12

Form Builder leaves beta with this release and requires Hyvä CMS 1.3.0. The upgrade rewrites every form, so read Upgrading to Hyvä Commerce 1.4.0 before running it.

Noteworthy change Forms have their own group in the Hyvä CMS ACL tree, and custom roles need the new Forms ACL resources granted after upgrading. Magento_Backend::content no longer gates the Form Builder screens, editor or REST routes. Its replacement is Hyvä CMS > Forms (Hyva_FormBuilder::form) with View, Save draft, Publish, Delete, Export and a Version history group holding Delete versions and Organise versions. The Forms grid, its menu entry, the editor's form list and the edit page need View; creating, duplicating, saving and inline editing a form need Save draft; deleting and mass deleting need Delete. REST routes follow the same tree: GET needs View; POST and PUT on /V1/hyvaForm need Publish; DELETE needs Delete; version history needs View, Organise versions and Delete versions; Tailwind JIT needs View and Save draft. The Form Builder configuration section and public form submission are unchanged. After upgrading, open System > Permissions > User Roles, edit each custom content role and tick the Forms resources it should have. Do the same for every REST integration with a custom resource list. Nothing is migrated automatically, and no code changes are needed. The Administrators role holds all resources and is unaffected.

Added

  • The Multi-Choice Field can render as a switch group, a third Display As option beside the checkbox group and the multi-select dropdown. It submits the same array, each input carries role="switch" and the theme styles it.
  • Defaults on the Select Field and Multi-Choice Field are ticked on the option rows in the editor instead of typed into a Default Value field. A Select Field takes one default, a Multi-Choice Field any number, and the default can no longer miss the option list. A form saved with the old comma-separated value is converted the next time the editor loads it, and the storefront keeps honouring the old value on content published before then.
  • Forms can be exported and imported through the Hyvä CMS Transfer Center, delivery settings (recipients, webhook, CAPTCHA) travel with the form.
  • Package-owned compiled CMS admin styles. The form editor dialogs, settings panel and custom field types ship their own stylesheet fragment, registered through the Hyvä CMS admin style contract, so the editor no longer depends on module-cms scanning this package's templates. Rebuild it with npm run build in src/view/adminhtml/tailwind after changing an admin template.
  • A CSS Classes field on the form, on each of its sections, and on each row, so utility classes can be added without a custom template.
  • A Layout field on the form, switching it between a single page and a multi-step wizard, saved and restored through version history like any other content change.
  • Forms can be assigned to store views. The create dialog's Advanced options and a new "Form in Websites" section in Form Settings carry the same website, store group and store view tree blocks and pages use, with All Store Views preselected and at least one tick required. The assignment is stored in a hyva_commerce_cms_form_store link table, exposed as stores on the REST payload and settable on save, and a save with no store ids is stored as All Store Views. The upgrade assigns every existing form to All Store Views. The Forms grid gains a Store View column and filter, the editor's store switcher reports the store views a form is not assigned to, and the editor's form listing groups forms by their real store views.
  • The create dialog derives the Identifier from the Title as you type, the way the page dialog derives its URL key. Typing into the Identifier breaks the link, clearing it restores it, and a blank Identifier on Create falls back to the Title's slug.
  • A form renders and accepts submissions only on the store views it is assigned to. The storefront renderer, behind both the Hyvä CMS Form component and the Magento widget, and the submission controller look the form up for the current store view, so an unassigned form renders nothing and a submission posted to it gets the existing not-found response. The REST lookup by identifier takes an optional store view id, getByIdentifier($identifier, $storeId = 0), and a second route GET /V1/hyvaForm/identifier/:identifier/store/:storeId returns the form a store view shows.
  • The same identifier can exist once per store view, the way CMS blocks work. A form on All Store Views is the default, a form with the same identifier assigned to a store view overrides it there, and a disabled override falls back to the default. Two forms sharing an identifier and a store view are refused on save, in the create dialog, in Form Settings and over REST, with "A form with identifier "%1" already exists in one of the selected store views." Duplicate copies the store set and still appends _copy.
  • The CMS Form component picks its form through the entity picker, the same modal blocks and posts use, with search and paging. It lists one row per form with the title, the identifier and the store views, and flags a disabled form "Not visible". The stored value is the picker's object and the storefront resolves it by identifier, so each store view shows its own form and placements saved before this change keep working without being reopened. The Magento widget's form option lists one entry per identifier, Title (identifier) or Title (identifier, N editions), and drops the (ID: n) suffix.
  • Export writes a form's store ids under stores and their store codes under store_codes, store 0 as admin. Import maps codes back to this installation's store views, falls back to the ids and then to All Store Views, and updates the form that holds the identifier on an overlapping store view rather than the first form with that identifier.
  • Preview URL Key and Generate Hyvä Styles in a form's settings. Both were stored already but never shown. With a Preview URL Key set, the editor previews the form on that storefront page.

Changed

  • New fields are optional by default. The Required setting on every field type now starts unticked, so tick it on the fields that need it. Existing fields keep their setting.
  • The Select Field is now labelled Single-Choice Field, to match the Multi-Choice Field and because it renders as radio buttons as well as a dropdown. Only the label changed, the form_field_select component key and saved forms are untouched.
  • The URL Field's Validation Pattern starts empty. The field already requires an absolute http(s) address in the browser and on the server, so the old ^https?://.+ default repeated that check. Saved forms keep their stored pattern.
  • Every field label now defaults to its component title, "Text Field", "Email Field", "Checkbox Field" and so on, replacing the mix of "Untitled field", "Website", "I agree" and similar. Fields already saved keep their label.
  • Storefront validation is one channel once Alpine hydrates. The form component marks its form novalidate on init, so the rule engine runs on every Send, flags every failing field at once, scrolls to the first failure and focuses its control. Until Alpine boots, and without JavaScript, the browser keeps its own checks. Requires a theme Tailwind rebuild and a static content flush: the required marker on grouped fields and the removal of the :user-invalid error stroke are CSS, and the stylesheet is served unversioned with a one-year cache lifetime, so hard reload as well.
  • A submission that fails validation answers HTTP 400 and a mail delivery failure HTTP 500, on the JSON and the HTML responses alike, so monitoring can tell a rejection from a success. The body keeps the { success, errors } shape and the honeypot response stays 200. The shipped form script paints per-field errors from a 400 as it did from a 200 envelope, and an error for a field with no visible control, a hidden field for one, lands in the form banner. A custom root that ships its own submit script and branches on response.ok should treat 400 as the validation envelope.
  • Every free-text field is bounded even when the merchant left the limit empty: 255 characters for text, phone and hidden fields, 254 for email, 2048 for URL, 10000 for textarea. A merchant limit may lower a ceiling, never raise it, and the ceiling reaches the client so pre-flight and server agree.
  • The URL field validates as a web address, an absolute http or https URL, in the browser and on the server, alongside the merchant pattern rather than instead of it. The client wraps a merchant pattern as ^(?:…)$ like the server and the browser's pattern attribute, and its email check uses the HTML specification's pattern.
  • Publishing refuses a validation pattern PCRE cannot compile and a Select or Multi-Choice default that matches none of the options, naming the field. A Select already published with an unmatched default renders its placeholder and pre-selects nothing, in the markup and in the Alpine state.
  • The CAPTCHA prompt, the send and security failure messages and the success fallback go through the translated message map instead of being English literals in the form script.
  • Breaking change: The unique index on the form identifier is dropped. Uniqueness is now the store view rule above, enforced by the resource model on every save.
  • The REST lookup by identifier now considers only active forms, so a disabled form answers 404 on GET /V1/hyvaForm/identifier/:identifier while GET /V1/hyvaForm/:id and the search route still return it. This is what lets a disabled store view edition fall back to the All Store Views form.
  • Breaking change: The two form roots, single_step_form and multi_step_form, are retired and replaced by one hyva_form root whose Layout field chooses between the single page and multi-step templates instead. The form root now renders in the editor as the components panel's header, an icon, a label and an edit pencil, rather than as an ordinary row, so it can no longer be dragged, ticked, copied, duplicated or deleted. Upgrading migrates every existing form, its draft content, its published content and every version history row, onto hyva_form with the matching layout, and also removes three legacy design keys, background colour, text colour and text alignment, from form roots; a form built before 2026-07-23 that couldn't be saved because its root carried text alignment becomes saveable again. Custom roots aren't migrated and have to accept Form Sections (form_step). On the storefront they render through the chosen Layout, or Single page if none is set, as in 0.1.0. Take a database backup before upgrading, it is the revert path, version history is not.
  • The success message and success CMS block no longer render through Alpine x-html, which the CSP build drops in Alpine 3.15. The form script paints the message as text and inserts the block itself instead. Breaking change for custom form roots: replace x-html="resultMessage" on the success div with x-ref="result", the resultMessage state is gone.

Fixed

  • Clicking a field inside a form embedded on a page or block opens the CMS Form component's panel, the way an embedded CMS Block or Menu does, instead of failing with a 404 in the console. The embed now wraps the form in the same nested content wrapper those entities carry, so the editor canvas strips the form's own component markers before binding clicks. Editor preview only, storefront markup is unchanged.
  • Every path that saves a form rejects an identifier outside ^[a-z0-9_]+$ or over 255 characters, not only the admin Save dialog. The check lives in the resource model, so the REST save, the grid inline edit, the Transfer Center import and duplication all get the same message and write nothing. A form created earlier through REST or an import with an identifier the pattern rejects keeps working until it is next saved, then that save reports the message. This includes the data patches that re-save existing forms on upgrade. Existing identifiers are not rewritten.
  • The Forms grid inline edit saved every column posted to it, not only the three the grid lets you edit, so a crafted request could write delivery settings or content without the editor's checks. It now sets only title, identifier and status and ignores anything else. A title over 255 characters, which the column used to truncate silently, is refused with the same message the settings dialog shows.
  • Two fields could share a field name until publish, where the editor showed a generic server error. A derived name that another field already uses now gets a numeric suffix, name_2, and duplicated or pasted fields are renamed the same way. A publish that still finds a clash is refused with the message kept in production, the toast stays until dismissed, and the clashing fields are marked in the editor. The same applies to the pattern and default-value publish checks.
  • A required Multi-Choice shown as a checkbox group and a required Select Field shown as radios show the red asterisk on their legend, and each checkbox or radio in a group announces the invalid state and the group's error message.
  • A save refused for lack of permission returns a 403 with its message. The form repository wrapped the refusal in a save error, which the editor reported as a 500 with no message and REST as a 400.
  • A required checkbox group recomputes its message on change, so ticking an option clears it and unticking every option brings it back before any submit. The multi-step Next button marks the fields it flags as touched, so their messages stay until corrected.
  • Form roots and the form widget now render their script and block on the calling template's layout instead of the request's shared layout, which inside the editor's Connect request could drop the sidebar re-render, so a new form's root never showed in the component tree and further adds were refused as over the root limit. Custom root templates should pass $block to renderForm() and renderFormScriptHtml().
  • The editor canvas no longer stacks a duplicate <form> on every root re-render of a saved form with no recipient. The envelope templates rendered the editor callouts before the <form> tag, so the canvas marker landed on the callout; the callouts now render inside the form.
  • The CMS Form component is no longer offered anywhere inside a form. Form Content declares excludes but no accepts, so the picker listed the whole CMS catalogue and a form could be nested in a form, either directly or inside a Grid one level further down. The server emitted two <form> tags, and because browsers drop the inner one while parsing, the embedded form's fields and its form_identifier were adopted by the outer submission. Needs the disallowed_components support in Hyvä CMS. A form saved with a nested CMS Form before this change keeps that node and has to be corrected by hand.

Removed

  • The data-pattern-msg attribute on text, URL and phone inputs. Nothing read it, the merchant's message reaches the client in the initial-state payload.

0.1.0 - 2026-08-11

The 0.1.0 release for Form Builder is the initial (beta) release. It lets you build forms in the Hyvä CMS Liveview Editor, embed them into any CMS page or block, and receive each submission by email. Requires Hyvä CMS 1.3.0-beta2 - see Installing Beta Releases.

Added

  • Forms as a first-class Hyvä CMS content type under Content > Elements > Forms, with drafts, publishing, and version-history rollback
  • Visual form building in the Liveview Editor with field types for text, email, phone, number, date, dropdowns, multi-choice, consent checkboxes, and hidden fields
  • Single-page and multi-step forms with Back / Next navigation
  • Email delivery of submissions with configurable recipients, reply-to, and success messages
  • Webhook deliveries: POST each submission to a webhook URL for Zapier, a CRM, or any other backend
  • Built-in honeypot spam protection on every form, plus optional Google reCAPTCHA per form
  • Test submissions from a preview, delivered as a real email marked as a test
  • Accessible, progressively enhanced frontend markup that still submits with JavaScript disabled
  • Embedding via widget into any CMS page or block
  • Developer extension points for custom field types, form roots, validation rules, security gates, and webhook deliveries