Skip to content

Introduction

Depending on their purpose, forms can have a lot of fields. To ease the cognitive load for the user, you can divide the form fields into several steps. You will be familiar with this principle from the checkout process of any webshop: beginning with the shopping cart, the user enters the billing address, followed by the delivery address and the desired payment method.

From a technical point of view, the use of multiple form pages in the TYPO3 Form Framework also simplifies the setup of optional fields using variants (conditions). For example, the step with the shipping address can be omitted if it is identical to the billing address.

If you create a multi-page form, you should always show the user where he is in the input process.

This progress indicator can be a simple colored bar showing the progress as a percentage, or a more detailed display with titles and, if necessary, numbering of the steps.

The setup of such an indicator is today's topic.

Starting point

Our example form represents a slightly simplified ordering process. Four pages are defined in the form:

  1. Product selection
  2. Billing address
  3. Shipping address (optional)
  4. Summary

The form step with the shipping address can be skipped by the user if he activates the checkbox "Use billing address as shipping address." on the previous page.

The screenshots below show the four form pages as a finished result.

There is a second part to this tutorial describing the setup of the highly customized summary page.

Templating

The templates are based on the Bootstrap 5 compatible Fluid templates from the Form Framework, which you can find at EXT:form/Resources/Private/FrontendVersion2/. Their usage must be configured with the YAML option templateVariant: version2.

Before we set up our new stepper navigation, we need to adjust the already existing form navigation. The buttons for the move forward/back are set up slightly different in the original template: in the condition with {form.previousPage}, a hidden input field is used for the index value of the page and further attributes.

This would be incompatible with the stepper navigation. Either the function of the stepper navigation or the back button will be broken.

Therefore, we add all the attributes of the input field directly to the button in our template override:

<nav class="{form.renderingOptions.formNavigation.navigationClassAttribute}" aria-label="{form.renderingOptions.formNavigation.navigationAriaLabelAttribute}">
    <f:if condition="{form.previousPage}">
        <f:form.button type="submit" property="__currentPage" value="{form.previousPage.index}" additionalAttributes="{respectSubmittedDataValue: false}" class="{form.renderingOptions.formNavigation.btnPreviousClassAttribute}" formnovalidate="formnovalidate">{formvh:translateElementProperty(element: form.currentPage, renderingOptionProperty: 'previousButtonLabel')}</f:form.button>
    </f:if>
    <f:if condition="{form.nextPage}">
        <f:then>
            <f:form.button property="__currentPage" value="{form.nextPage.index}" class="{form.renderingOptions.formNavigation.btnNextClassAttribute}">{formvh:translateElementProperty(element: form.currentPage, renderingOptionProperty: 'nextButtonLabel')}</f:form.button>
        </f:then>
        <f:else>
            <f:form.button property="__currentPage" value="{form.pages -> f:count()}" class="{form.renderingOptions.formNavigation.btnSubmitClassAttribute}">
                {formvh:translateElementProperty(element: form, renderingOptionProperty: 'submitButtonLabel')}
            </f:form.button>
        </f:else>
    </f:if>
</nav>

Templates/Form.html

Now we can include a new partial in the form template for the stepper navigation:

<formvh:renderRenderable renderable="{form}">
    <formvh:form
        object="{form}"
        action="{form.renderingOptions.controllerAction}"
        method="{form.renderingOptions.httpMethod}"
        id="{form.identifier}"
        section="{form.identifier}"
        enctype="{form.renderingOptions.httpEnctype}"
        addQueryString="{form.renderingOptions.addQueryString}"
        argumentsToBeExcludedFromQueryString="{form.renderingOptions.argumentsToBeExcludedFromQueryString}"
        additionalParams="{form.renderingOptions.additionalParams}"
        additionalAttributes="{formvh:translateElementProperty(element: form, property: 'fluidAdditionalAttributes')}"
    >
        <div class="mb-5">
            <f:render partial="Form/MultiStepNavigation" arguments="{form: form}"/>
        </div>
        <f:render partial="{form.currentPage.templateName}" arguments="{page: form.currentPage}" />
        <div class="{form.renderingOptions.formNavigation.navigationWrapperClassAttribute}">
            <f:render partial="Form/Navigation" arguments="{form: form}" />
        </div>
    </formvh:form>
</formvh:renderRenderable>

Make sure that you include the partial inside the formvh:form ViewHelper. Otherwise your navigation will not work.

Partials/Form/MultiStepNavigation.html

This brings us to the actual stepper navigation:

<nav>
    <ol class="c-stepper">
        <f:for each="{form.pages}" as="page" iteration="step">
            <f:if condition="{page.index} == {form.currentPage.index}">
                <f:then>
                    <f:comment><!-- Current page (not linked) --></f:comment>
                    <li class="c-stepper__item c-stepper__item--current">
                        <div class="c-stepper__circle">
                            <span class="c-stepper__label" aria-current="page">{formvh:translateElementProperty(element: page, property: 'label')}</span>
                        </div>
                    </li>
                </f:then>
                <f:else>
                    <f:if condition="{page.index} < {form.currentPage.index}">
                        <f:then>
                            <f:comment><!-- Previous pages --></f:comment>
                            <li class="c-stepper__item">
                                <f:form.button property="__currentPage" value="{page.index}"
                                               class="u-button-reset c-stepper__circle"
                                               additionalAttributes="{respectSubmittedDataValue: false}"
                                               formnovalidate="formnovalidate">
                                    <span class="c-stepper__label">{formvh:translateElementProperty(element: page, property: 'label')}</span>
                                </f:form.button>
                            </li>
                        </f:then>
                        <f:else>
                            <f:comment><!-- Upcoming pages --></f:comment>
                            <li class="c-stepper__item c-stepper__item--next">
                                <div class="c-stepper__circle">
                                    <span class="c-stepper__label">{formvh:translateElementProperty(element: page, property: 'label')}</span>
                                </div>
                            </li>
                        </f:else>
                    </f:if>
                </f:else>
            </f:if>
        </f:for>
    </ol>
</nav>

The styling of the stepper navigation does not depend on Bootstrap. Feel free to use the stylesheet of the demo as a basis for your project.

How the stepper navigation works

  • Previous, already completed pages can be opened directly by the user with this navigation. The rendered button uses the index value of these pages.
  • The current page is not linked (superfluous).
  • Upcoming pages are not linked, even if the user did already fill out the complete form and then returned to a previous page. The reason is that the Form Framework must always evaluate each form step.

Demo

The TYPO3 extension "form_multistep" provides a complete demo setup. Besides the form and the necessary templates, I've also added a basic TypoScript configuration with PAGE object this time. You can simply include the TypoScript of the extension in a new page tree to get a (somewhat) appealing design with Bootstrap, as well as custom styles for the stepper navigation.

The form configuration is automatically registered in TYPO3 when you install the extension. Since all configurations are defined within a new form prototype, this should not interfere with your existing forms.

EXT:form_multistep on GitHub

The extension was tested with TYPO3 v11 and v12.