Skip to content

Add a Form Layout

A form layout is another way of rendering the shipped Form root, hyva_form. The Form Builder ships two layouts, Single page and Multi-step, and a module adds its own with one template plus one registry entry on Hyva\FormBuilder\Model\Config\Source\FormLayouts. This guide builds an accordion layout that renders each Form Section as its own panel.

A layout is enough when the form is still Form Sections holding fields, just laid out differently. The shipped root keeps owning everything else: the Form Section children, the component-side settings from the shared metadata includes, and the whole submission pipeline. When the form is structurally different, with different children, settings of its own, or its own <form> envelope, you need a new root instead. Create a Custom Form Root covers that advanced path.

Prerequisites

You should be comfortable with Magento DI (di.xml array arguments) and Hyvä templates. A layout needs no component declaration, because it renders the shipped hyva_form root. The Form Builder ships as hyva-themes/commerce-module-form-builder; your module should depend on it. The Architecture page explains how the root's settings reach the submission pipeline.

How Form Layouts Work

The shipped root's Layout field is a variants field whose options come from the FormLayouts registry. Each option's value is a template path. Choosing a layout stores that path on the root's variants property, and the editor and the storefront both render the form through that template.

The same registry feeds the new-form dialog's type step. Every registered layout appears there as a card, ahead of any third-party root, and choosing one seeds a hyva_form root set to that layout with a first Form Section inside it. So one di.xml item makes a layout available in both places, with no plugin.

On the storefront, Hyva\FormBuilder\Block\Form checks the stored value against the registered layouts before calling setTemplate(). A blank or unregistered value falls back to FormLayouts::DEFAULT_LAYOUT, the Single page template, so a form whose layout module is later removed still renders, as a single page.

The Layout field itself is opt-in

hyva_form gets a Layout field because its declaration lists Hyva_FormBuilder::etc/hyva_cms/form_root_layout_metadata.json in content.includes, ahead of the shared form_root_content_metadata.json. A custom root doesn't get one unless it includes that file too. Without it, the root has no Layout value, so the storefront renders it through the Single page layout. See Create a Custom Form Root.

Step 1: Register the Layout

Add an item to the layouts array argument on FormLayouts in your module's di.xml. The shipped single_page and multi_step items in module-form-builder/src/etc/di.xml have the same shape:

app/code/Acme/AccordionLayout/etc/di.xml
<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Hyva\FormBuilder\Model\Config\Source\FormLayouts">
        <arguments>
            <argument name="layouts" xsi:type="array">
                <item name="accordion" xsi:type="array">
                    <item name="value" xsi:type="string">Acme_AccordionLayout::elements/accordion_form.phtml</item>
                    <item name="label" xsi:type="string" translate="true">Accordion</item>
                    <item name="description" xsi:type="string" translate="true">Every section collapsed into its own panel, opened one at a time.</item>
                    <item name="icon" xsi:type="string">Acme_AccordionLayout::images/accordion-form.svg</item>
                    <item name="sort_order" xsi:type="number">30</item>
                </item>
            </argument>
        </arguments>
    </type>
</config>

Magento merges array arguments by item name, so accordion slots in alongside the two shipped layouts without colliding. Each item carries five keys:

  • value - the template path. It's what gets stored on the root when the layout is chosen, and what the storefront block checks before rendering. An item with a blank value is skipped.
  • label and description - the Layout field's option text and the type card in the new-form dialog. Both are translated.
  • icon - the type card's image, resolved as a view file in the admin area, so the SVG lives at view/adminhtml/web/images/accordion-form.svg in your module.
  • sort_order - the position among the layouts, ascending. The shipped pair use 10 and 20.

Step 2: Ship the Layout Template

The value path resolves like any Magento template, so Acme_AccordionLayout::elements/accordion_form.phtml lives at view/frontend/templates/elements/accordion_form.phtml in your module.

A layout template is a complete root template. It renders the whole <form> envelope for hyva_form, not just the sections, so start from a copy of whichever shipped layout is closer rather than from scratch:

  • module-form-builder/src/view/frontend/templates/elements/single_page_form.phtml - every section stacked on one page, one submit button.
  • multi_step_form.phtml beside it - one section at a time, adding the progress bar, the step indicator and Back and Next navigation.

The two files differ only in their section handling and navigation markup. Everything else in them is shared envelope that your layout keeps.

What a Layout Template Renders

Read either shipped file end to end before adapting it. A layout template keeps these pieces:

  • The three render paths. The branch near the top of the file resolves the storefront ($block->getPublishedForm() returns a PublishedForm), the form's own full-screen preview (live submissions), and the editor canvas (an inert submit button). Keep the branch as it is. The table in Ship the Root Template explains each path.
  • The <form> wired to the form script. x-data="hyvaFormBuilder", @submit.prevent="submitForm", and the <script type="application/json" data-initial-state> element carrying the state seed from FormRenderer. Inside it sit the hidden form_key and form_identifier inputs, the storefront-only honeypot input, the reCAPTCHA fragments, the data-hyva-form-error="_form" region for form-level errors, and the editor-canvas callouts. The root's CSS Classes value arrives as $block->getData('classes') and goes on the <form> tag.
  • The Form Section children, stamped. Each form_step child gets a step_index and a form_layout before it renders through ChildElementRenderer::renderChildHtml().
  • The success surface. The data-hyva-form-result="success" element carrying x-ref="result" is where the form script paints the success message or Success Content block. Drop it, or its x-ref, and a successful submission leaves the customer looking at nothing.
  • The form script, once. The file ends with $formRenderer->renderFormScriptHtml($block), after the closing </form>.

Stamping the Form Section Children

form_step.phtml reads the form_layout value its parent stamps. multi_step wires the section into the wizard, showing only the current one and giving a blank section the label "Step N". Any other value shows every section and leaves a blank label without a legend. The index keeps each section's data-step-index in step with the DOM.

An accordion shows every section and does its own collapsing, so it stamps the same values as the Single page layout:

view/frontend/templates/elements/accordion_form.phtml (excerpt)
$indexedChildren = [];
$sectionCounter = 0;
foreach ($children as $child) {
    $childArray = (array)$child;
    if (($childArray['component'] ?? '') === 'form_step') {
        $childArray['step_index'] = $sectionCounter;
        // Anything other than multi_step renders every section, which the accordion wants.
        $childArray['form_layout'] = 'single_page';
        $sectionCounter++;
    }
    $indexedChildren[] = $childArray;
}

The panel markup then wraps each rendered child. The shared form script knows only the Multi-step layout's step state, so opening and closing panels is up to your own markup.

Passing the Block to the Form Script

renderFormScriptHtml() takes the calling block, and the shipped templates pass $block. The script then renders on the caller's layout: the page layout on the storefront, and in the editor preview a layout other than the shared one the editor's sidebar render depends on. A named layout block dedupes the script, so several forms on one page still emit it once. Keep the argument when you copy the call.

Reference Implementation

In the module-form-builder package:

  • src/etc/di.xml - the FormLayouts wiring for the two shipped layouts.
  • src/Model/Config/Source/FormLayouts.php - the registry, DEFAULT_LAYOUT, and the check the storefront makes.
  • src/Block/Form.php - how the stored layout is validated before setTemplate().
  • src/ViewModel/Adminhtml/ContentSetup/FormTypes.php - how layouts become the new-form dialog's type cards.
  • src/view/frontend/templates/elements/single_page_form.phtml and multi_step_form.phtml
  • the two shipped layout templates.
  • src/view/frontend/templates/elements/form_step.phtml - how a section reads the stamped form_layout and step_index.