Handlebars Forms 

Extension key

handlebars_forms

Package name

cpsit/typo3-handlebars-forms

Version

main

Language

en

Author

coding. powerful. systems. CPS GmbH

License

This extension documentation is published under the CC BY-NC-SA 4.0 (Creative Commons) license.


An extension for TYPO3 CMS that provides form rendering with Handlebars, based on the EXT:handlebars extension. It is designed to provide rendering options for all default form elements shipped by EXT:form, while rendering for custom form elements can be easily configured using dedicated interface implementations.


Introduction 

A quick overview about the main features provided by this extension.

Installation 

Instructions on how to install this extension, and which TYPO3 and PHP versions are currently supported.

Configuration 

Learn how to configure the extension in various ways. This includes site configuration and TypoScript configuration.

Usage 

This section describes how to use this extension in various ways.

Developer corner 

A quick overview about all relevant classes provided by this extension.

Migration 

Required migration steps when upgrading the extension to a new major version.

Introduction 

What does it do? 

The extension provides a way to render forms built with the TYPO3 Form Framework. It allows rendering of generic forms, defined by a comprehensive TypoScript rendering definition. This definition can be extended to a specific form to allow customizating the output of various forms. In addition, the extension allows to modify the build mechanisms of so called view models, which makes the whole concept very dynamic and flexible.

Features 

  • Support for all default form elements
  • Ability to define generic form rendering definitions
  • Possibility to override form rendering for specific form definitions
  • Easy to extend and customize for custom form elements
  • Compatible with TYPO3 13.4 LTS and 14.3 LTS

Support 

There are several ways to get support for this extension:

License 

This extension is licensed under GNU General Public License 2.0 (or later).

Installation 

Requirements 

  • PHP 8.2 - 8.5
  • TYPO3 13.4 LTS - 14.3 LTS

Installation 

Require the extension via Composer (recommended):

composer require cpsit/typo3-handlebars-forms
Copied!

Or download it from the TYPO3 extension repository.

Site set 

The extension ships a site set that provides default TypoScript configuration and site settings. Include it in your site's config.yaml:

config/sites/<identifier>/config.yaml
dependencies:
  - cpsit/handlebars-forms
Copied!

The site set requires two other sets as dependencies: cpsit/handlebars-content-element (from EXT:handlebars) and typo3/form (from EXT:form). Both are declared in the extension's own set, so you do not need to list them separately.

Configuration 

Site settings 

The following settings are exposed through the site set and can be overridden per site.

Name Type Default
string Form

handlebars_forms.view.templateName

handlebars_forms.view.templateName
Type
string
Default
Form

Name of the Handlebars template used for rendering a form when no per-form template is configured. Corresponds to a template file in the configured Handlebars template root paths, e.g. Form.hbs.

TypoScript 

The site set sets up a TypoScript object at plugin.tx_form.handlebarsForms, where integrators define how each form is rendered. The structure is:

plugin.tx_form.handlebarsForms {
    # Applied to every form (fallback)
    default {
        templateName = {$handlebars_forms.view.templateName}

        dataProcessing {
            10 = process-form
            10 {
                # ... HBS_* configuration
            }
        }
    }

    # Override for a specific form – merged on top of "default"
    my_contact_form {
        templateName = ContactForm
    }
}
Copied!

When a form is rendered, the extension looks up configuration blocks in the following order. All matching blocks are merged, with later entries winning:

  1. default – applied to every form
  2. Form unique identifier (e.g. my-contact-form-123)
  3. Original form identifier before suffixes are appended (e.g. my-contact-form)
  4. Form persistence identifier (the YAML file path, e.g. EXT:my_extension/Resources/Private/Forms/ContactForm.form.yaml)

The templateName key names the Handlebars template file (without extension) that receives the data produced by the process-form processor as its template context.

See Conditions in the processor reference for details on using if to conditionally omit keys from the output.

Usage 

This section explains how to set up and use EXT:handlebars_forms in a TYPO3 project.

The extension works in two phases:

  1. Form renderer HandlebarsFormRenderer is registered with EXT:form. When a form is rendered on the frontend, EXT:form delegates rendering to this class, which resolves a Handlebars view based on the TypoScript configuration under plugin.tx_form.handlebarsForms.
  2. Data processor – The Handlebars template is expected to call the process-form data processor. This processor walks the EXT:form renderable tree and builds a plain PHP array from it using HBS_* content objects. The resulting array is passed to the Handlebars template as its context.

Quick start 

This page walks through the minimum steps required to render a form with Handlebars.

  1. Include the site set

    Follow the site set instructions in the installation guide to add cpsit/handlebars-forms to your site's dependencies.

  2. Configure TypoScript

    Add a dataProcessing block under plugin.tx_form.handlebarsForms.default that maps EXT:form renderables to HBS_* content objects. The array built by the processor becomes the Handlebars template context.

    The example below produces a fields array and a navItems array from the current form page, plus a hiddenFields string:

    plugin.tx_form.handlebarsForms {
        default {
            dataProcessing {
                10 = process-form
                10 {
                    formData {
                        id = HBS_TAG
                        id.attribute = id
    
                        action = HBS_TAG
                        action.attribute = action
    
                        method = HBS_TAG
                        method.attribute = method
                    }
    
                    fields = HBS_RENDERABLES
                    fields {
                        default {
                            template = @form-field-generic
    
                            id = HBS_TAG
                            id.attribute = id
    
                            name = HBS_TAG
                            name.attribute = name
    
                            label = HBS_LABEL
    
                            value = HBS_TAG
                            value.attribute = value
                        }
    
                        # Per-type overrides inherit from default via TypoScript copy operator
                        Text < .default
                        Text {
                            template = @form-field-text
                        }
    
                        Email < .Text
    
                        # Suppress Honeypot in the template; render it verbatim instead
                        Honeypot {
                            content = HBS_PASSTHROUGH
                        }
                    }
    
                    navItems = HBS_NAVIGATION
                    navItems {
                        previousPage {
                            label = HBS_LABEL
    
                            name = HBS_TAG
                            name.attribute = name
    
                            value = HBS_TAG
                            value.attribute = value
                        }
    
                        nextPage < .previousPage
                        submit < .previousPage
                    }
    
                    hiddenFields = HBS_TAG
                }
            }
        }
    }
    Copied!
  3. Create a Handlebars template

    Create a .hbs file in the Handlebars template root path configured by your EXT:handlebars installation. The default template name is Form (configurable via the handlebars_forms.view.templateName site setting), so the file should be named Form.hbs.

    The template receives the data built by the process-form processor directly as its context:

    <form id="{{formData.id}}"
          method="{{formData.method}}"
          action="{{formData.action}}"
    >
        {{#each fields}}
            {{#if template}}
                {{> (lookup . 'template')}}
            {{elseif content}}
                {{this.content}}
            {{/if}}
        {{/each}}
    
        {{#each navItems}}
            {{> '@button' this}}
        {{/each}}
    
        {{hiddenFields}}
    </form>
    Copied!

Per-form overrides 

To use a different template or a different data structure for a specific form, add a block keyed by the form identifier. It is merged on top of default:

plugin.tx_form.handlebarsForms {
    my_contact_form {
        templateName = ContactForm

        dataProcessing {
            10 = process-form
            10 {
                fields =< plugin.tx_form.handlebarsForms.default.dataProcessing.10.fields
                fields {
                    # Extra field type only present in this form
                    Rating {
                        template = @form-field-rating

                        value = HBS_TAG
                        value.attribute = value
                    }
                }
            }
        }
    }
}
Copied!

process-form processor 

The process-form data processor is the core of the extension. It resolves each key in its TypoScript configuration through content objects in the context of the top-level renderable ( FormRuntime), producing a plain PHP array that becomes the Handlebars template context.

The data processor is registered under the identifier process-form and can be used inside a dataProcessing block:

plugin.tx_form.handlebarsForms.default {
    dataProcessing {
        10 = process-form
        10 {
            # processor configuration
        }
    }
}
Copied!

Key resolution 

The processor iterates all keys in its configuration block and resolves each one by the following rules, applied in order:

  1. Array value (no string sibling) – the sub-tree is processed recursively. The result is stored as a nested array under the key name (without trailing dot).
  2. String value matching a registered content object – the content object is rendered with the dotted sub-tree as its configuration. The return value is stored under the key name.
  3. String value not matching any content object – the raw string is stored as-is.

These rules mean the structure of the TypoScript block directly mirrors the structure of the resulting PHP array:

10 = process-form
10 {
    # Rule 1: nested array
    formData {
        id = HBS_TAG
        id.attribute = id

        method = HBS_TAG
        method.attribute = method
    }

    # Rule 2: content object
    label = HBS_LABEL

    # Rule 3: literal string
    staticKey = staticValue
}
Copied!

Produces:

[
    'formData' => [
        'id'     => '…',   // resolved by HBS_TAG
        'method' => '…',   // resolved by HBS_TAG
    ],
    'label'     => '…',    // resolved by HBS_LABEL
    'staticKey' => 'staticValue',
]
Copied!

Content object configuration 

Configuration for a content object is placed in the dotted sub-tree below the key:

id = HBS_TAG
id.attribute = id

subject = HBS_PROPERTY
subject.path = type
subject.subject = renderable
Copied!

The dotted sub-tree also supports stdWrap, which is applied to the resolved value after the content object returns:

label = HBS_LABEL
label.stdWrap.case = upper
Copied!

Conditions 

if is supported at two levels and behaves like the built-in TypoScript function.

It can be placed directly in the processor configuration (or in any nested sub-tree). When the condition evaluates to false, the entire block is skipped and null is returned for its parent key:

fields = HBS_RENDERABLES
fields {
    Fieldset {
        if.isTrue = 0
    }
}
Copied!

The currentValue extension in if allows the condition to be evaluated against a value resolved by the processor itself rather than a static TypoScript value. It accepts a content object expression using the same syntax as the main configuration:

someField {
    if {
        currentValue = HBS_PROPERTY
        currentValue.path = required
        isTrue.current = 1
    }

    label = HBS_LABEL

    value = HBS_TAG
    value.attribute = value
}
Copied!

TypoScript references 

TypoScript copy ( <) and reference ( =<) operators are fully supported. References are resolved before any key in a block is processed, so the merged configuration is what the content objects see:

Email < .Text

navItems = HBS_NAVIGATION
navItems {
    previousPage {
        # ...
    }
    nextPage < .previousPage
    submit < .previousPage
}

fields =< plugin.tx_form.handlebarsForms.default.dataProcessing.10.fields
Copied!

Renderable context 

Each key in the processor configuration is resolved in the context of the current renderable. At the top level this is the FormRuntime (i.e. the whole form). When HBS_RENDERABLES iterates children, each child key is resolved in the context of that child renderable and its view model.

The active renderable and view model are accessible to all HBS_* content objects through the ValueResolutionContext they receive. This is what allows HBS_TAG to read from the rendered tag of the current element rather than a global one, and why the same HBS_LABEL call returns a different label for each iteration of HBS_RENDERABLES.

Content objects 

The extension registers the following custom content objects ( HBS_*). They are only useful inside a process-form data processor block; using them elsewhere may log a warning and return an empty string.

Every HBS_* content object supports the standard stdWrap sub-key. The value resolved by the content object is passed through stdWrap before being stored in the processed data array.


HBS_RENDERABLES 

Iterates the child renderables of the current renderable and resolves each one according to per-type configuration. Returns a list (array).

When used at the top level (form runtime as current renderable), it iterates the elements of the current page. For composite renderables such as Fieldset, it iterates their direct children.

Configuration

fields = HBS_RENDERABLES
fields {
    # Per-type configuration (key = EXT:form element type)
    Text {
        template = @form-field-text
        label = HBS_LABEL
        value = HBS_TAG
        value.attribute = value
    }

    # Fallback for types without a dedicated block
    default {
        template = @form-field-generic
    }

    # Single content object for a type (no sub-configuration)
    Honeypot = HBS_PASSTHROUGH

    # Suppress a type entirely
    SomeType {
        if.isTrue = 0
    }
}
Copied!

The lookup order for each child element is: exact type key, default. If neither matches, the element is skipped.

Frontend register

While iterating, HBS_RENDERABLES writes two values to the frontend register that TypoScript conditions can read via register:

  • HBS_RENDERABLES_COUNT – total number of renderables in the current iteration
  • HBS_RENDERABLES_CURRENT – zero-based index of the element being processed (unset after the loop)

HBS_PROPERTY 

Reads a property from the current renderable, its view model, or the form runtime using Extbase\Reflection\ObjectAccess::getProperty(). Returns whatever type the property holds (string, array, object, …).

Configuration

Name Type Default
string (none)
string renderable

path

path
Type
string
Default
(none)

Property path. Supports dotted-path notation for nested access (e.g. renderingOptions.foo).

subject

subject
Type
string
Default
renderable

The object to read from. One of:

  • renderable – the current EXT:form renderable (default)
  • viewModel – the view model built for this renderable
  • formRuntime – the form runtime instance

Example

renderableType = HBS_PROPERTY
renderableType.path = type

# Read from the view model instead
resourcePointerFields = HBS_PROPERTY
resourcePointerFields {
    subject = viewModel
    path = children?[resourcePointerFields?]
}
Copied!

HBS_TAG 

Reads an HTML attribute (or the inner content) from the tag rendered by the current renderable's view model. The view model must implement TagAwareViewModel; this is the case for all view models built by the built-in ViewModelBuilder implementations. Returns a SafeString (Handlebars will not escape the value).

The tag reflects the final output of the Fluid ViewHelper responsible for rendering the renderable. For example, the root FormRuntime object is rendered by the <formvh:form> view helper, so HBS_TAG on it returns attributes (or content) of the <form> tag that view helper produces. Similarly, a Text element is rendered by <f:form.textfield>, so HBS_TAG exposes the attributes of the resulting <input> tag.

Configuration

Name Type Default
string (none)

attribute

attribute
Type
string
Default
(none)

Name of the HTML attribute to read. If omitted, the inner content of the rendered tag is returned instead.

Example

# Read the "id" attribute of the rendered <input> tag
id = HBS_TAG
id.attribute = id

# Read the inner HTML of the rendered tag (e.g. <textarea> content)
content = HBS_TAG
Copied!

HBS_LABEL 

Returns the translated label of the current renderable. If the current view model is a FormFieldViewModel, the label is taken from the view model's pre-resolved label property; otherwise it falls back to $renderable->getLabel(). Returns a string.

Configuration

No configuration keys. stdWrap is supported.

label = HBS_LABEL
Copied!

HBS_FORM_VALUE 

Reads the current or submitted value of the form element. Internally wraps EXT:form's <formvh:renderFormValue> view helper and exposes its result as a FormValueViewModel.

Without an output key the resolved processedValue is returned directly.

Output instructions

The output key accepts one of the following built-in instructions:

Name Type
string instruction
string instruction
string instruction
string instruction
string instruction
string instruction

PROCESSED_VALUE

PROCESSED_VALUE
Type
string instruction

The formatted, human-readable value (e.g. option label for select fields).

VALUE

VALUE
Type
string instruction

The raw, machine-readable value.

IS_MULTI_VALUE

IS_MULTI_VALUE
Type
string instruction

Boolean – whether the field holds multiple values (e.g. multi-select, multi-checkbox).

IS_SECTION

IS_SECTION
Type
string instruction

Boolean – whether the renderable is a section (fieldset, page).

EACH_PROCESSED_VALUE

EACH_PROCESSED_VALUE
Type
string instruction

Iterates over all values and processes each with the sub-configuration in output.

EACH_VALUE

EACH_VALUE
Type
string instruction

Like EACH_PROCESSED_VALUE but uses raw values.

Examples

# Summary page: show the human-readable value
value = HBS_FORM_VALUE
value.output = PROCESSED_VALUE

# Multi-value field: iterate over each option
values = HBS_FORM_VALUE
values {
    output = EACH_PROCESSED_VALUE
    output {
        label = HBS_LABEL

        selected = HBS_PROPERTY
        selected.path = selected
    }
}
Copied!

HBS_NAVIGATION 

Resolves the navigation buttons (previous page, next page / submit) for the current form page. Returns a list of processed items. Each item is built by the sub-configuration keyed by button role.

Button roles

  • previousPage – previous-page button (only present when not on the first page)
  • nextPage – next-page button (only present when not on the last page)
  • submit – submit button (only present on the last page)

Within each role block, HBS_TAG and HBS_LABEL operate on the rendered <button> tag and the translated button label respectively.

Example

navItems = HBS_NAVIGATION
navItems {
    previousPage {
        label = HBS_LABEL

        name = HBS_TAG
        name.attribute = name

        value = HBS_TAG
        value.attribute = value
    }

    nextPage < .previousPage
    submit < .previousPage
}
Copied!

HBS_PASSTHROUGH 

Renders the current renderable using EXT:form's standard Fluid partials and returns the result as a SafeString. Useful for elements that do not need a custom template (e.g. Honeypot, ContentElement) or as a fallback.

Configuration

Additional TypoScript keys are converted to plain PHP variables and passed to the Fluid rendering context:

Honeypot {
    content = HBS_PASSTHROUGH
}

# Pass extra variables to the Fluid partial
SomeElement {
    content = HBS_PASSTHROUGH
    content {
        myVariable = someValue
    }
}
Copied!

HBS_CHILDREN 

Returns the children of the current view model as a list. The view model must implement CompositeViewModel (e.g. ViewModelCollection). Returns null when the view model has no children.

Each child is processed using the sub-configuration of HBS_CHILDREN.

Frontend register

While iterating, HBS_CHILDREN writes two values to the frontend register that TypoScript conditions can read via register:

  • HBS_CHILDREN_COUNT – number of children
  • HBS_CHILDREN_CURRENT – index of the child being processed (unset after the loop)

Example

options = HBS_CHILDREN
options {
    label = HBS_LABEL

    checked = HBS_TAG
    checked.attribute = checked
}
Copied!

HBS_TRANSLATE_PROPERTY 

Translates a renderable property using EXT:form's <formvh:translateElementProperty> view helper.

Configuration

Name Type Default
string (required)
string property

property

property
Type
string
Default
(required)

Name of the element property to translate.

argumentName

argumentName
Type
string
Default
property

Argument name passed to the view helper. Use renderingOptionProperty to translate a rendering option rather than a regular property.

Example

placeholder = HBS_TRANSLATE_PROPERTY
placeholder.property = placeholder

submitButtonLabel = HBS_TRANSLATE_PROPERTY
submitButtonLabel {
    property = submitButtonLabel
    argumentName = renderingOptionProperty
}
Copied!

HBS_TRANSLATE_ERROR 

Returns the translated message for a specific validation error code on the current renderable. Uses EXT:form's <formvh:translateElementError> view helper internally.

Configuration

Name Type Default
int (required)

errorCode

errorCode
Type
int
Default
(required)

Numeric validation error code (e.g. 1221560718 for NotEmpty).

Example

requiredError = HBS_TRANSLATE_ERROR
requiredError.errorCode = 1221560718
Copied!

HBS_VALIDATION_RESULTS 

Returns the Extbase validation results for the current renderable. Without an output instruction the raw Result object is returned.

Output instructions

Name Type
string instruction
string instruction
string instruction
string instruction

EACH_ERROR

EACH_ERROR
Type
string instruction

Iterates over every error and processes each with the sub-configuration in output. On composite renderables the result is a dictionary keyed by property path; on leaf elements it is a flat list.

EACH_RENDERABLE

EACH_RENDERABLE
Type
string instruction

Iterates over renderables that have at least one error. Processes each with the sub-configuration, then passes the result through a second round of process-form resolution so nested HBS_* objects are resolved too. The result is a dictionary keyed by property path.

ERROR_MESSAGE

ERROR_MESSAGE
Type
string instruction

Returns the translated message for the first error in the result set.

RESULT

RESULT
Type
string instruction

Returns a property from the Result object. Requires output.propertyPath to be set.

Example

errors = HBS_VALIDATION_RESULTS
errors {
    output = EACH_RENDERABLE
    output {
        label = HBS_LABEL

        message = HBS_VALIDATION_RESULTS
        message.output = ERROR_MESSAGE
    }
}
Copied!

HBS_VH_CONTENT 

Returns the output of the view helper that was used to build the current view model. The view model must be a ViewHelperContainedViewModel. HTML strings are returned as SafeString (no double-escaping).

No configuration keys beyond stdWrap.

Example

# Render the raw <input> tag produced by the view helper
content = HBS_VH_CONTENT
Copied!

View models 

Renderables are converted to so-called view models, which is done by dedicated view model builders. Each view model represents the default view implementation of a renderable, based on the Fluid templates shipped by EXT:form. Note that some renderables might be represented by various view model implementations, based on specific aspects outlined below.

The following renderables are currently supported by this extension:

Supported renderables 

AdvancedPassword 

(a) ViewModelCollection

Contains view models, reflecting both password fields:

Name Type Description
passwordField ViewHelperContainedViewModel Contains result from <formvh:form.password> view helper invocation for password field.
confirmationField (a) ViewHelperContainedViewModel Result from <formvh:form.password> view helper invocation for password confirmation field.
(b) FormFieldViewModel Combination of confirmation label and result from <formvh:form.password> view helper invocation for password confirmation field.

Checkbox 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.checkbox> view helper invocation.

ContentElement 

(a) ViewHelperContainedViewModel
Contains result from <f:cObject> view helper invocation.
(b) SimpleViewModel
If configured content element UID is invalid.

CountrySelect 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.countrySelect> view helper invocation.

Fieldset 

(a) StandaloneTagViewModel
Contains the <fieldset> tag with class name(s) and additional attributes.

FileUpload, ImageUpload 

(a) ViewModelCollection

If uploaded resource can be resolved. Contains three view models:

Name Type Description
uploadField ViewHelperContainedViewModel Contains result from <formvh:form.uploadedResource> view helper invocation for password field.
resourcePointerFields ViewModelCollection of StandaloneTagViewModel Optional. References hidden <input> fields with resource pointers, if available.
uploads ViewModelCollection of FileResourceViewModel

References file uploads, which contain one or two child view models:

  • resource: Instance of FileReference or PseudoFileReference.
  • deleteCheckbox: Optional and TYPO3 >= v14 only. FormFieldViewModel with result from <formvh:form.uploadDeleteCheckbox> view helper invocation, which allows to delete an existing file upload on submit.
(b) ViewHelperContainedViewModel
If uploaded resource cannot be resolved. Contains result from <formvh:form.uploadedResource> view helper invocation.

Form 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form> view helper invocation.

Hidden 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.hidden> view helper invocation.

MultiCheckbox 

(a) ViewModelCollection

Contains view models which reflect all available options, each as one of:

Type Description
FormFieldViewModel If label is available. Contains a combination of label and result from <formvh:form.checkbox> view helper invocation.
ViewHelperContainedViewModel If associated label is invalid or missing. Contains result from <formvh:form.checkbox> view helper invocation.

Password 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.password> view helper invocation.

RadioButton 

(a) ViewModelCollection

Contains view models which reflect all available options, each as one of:

Type Description
FormFieldViewModel If label is available. Contains a combination of label and result from <formvh:form.radio> view helper invocation.
ViewHelperContainedViewModel If associated label is invalid or missing. Contains result from <formvh:form.radio> view helper invocation.

SingleSelect, MultiSelect 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.select> view helper invocation. Includes available <option> tags as children of type StandaloneTagViewModel.

StaticText 

(a) FormFieldViewModel
If label is available. Contains a combination of label and <p> tag.
(b) StandaloneTagViewModel
If label is invalid or missing. Contains <p> tag with class and text.

Textarea 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.textarea> view helper invocation.

Text, Date, Email, Number, Telephone, Url 

(a) ViewHelperContainedViewModel
Contains result from <formvh:form.textfield> view helper invocation.

Unsupported renderables 

DatePicker 

This form element was deprecated in TYPO3 v14.2 and is therefore not supported by this extension.

Custom view model builders 

Every EXT:form renderable that reaches HBS_RENDERABLES is converted to a view model before TypoScript processes it. The conversion is handled by view model builders: classes that implement \CPSIT\Typo3HandlebarsForms\Domain\ViewModel\Builder\ViewModelBuilder.

When no registered builder claims a renderable, a plain SimpleViewModel is used as the fallback.

Implementing the interface 

The ViewModelBuilder interface declares two methods:

interface ViewModelBuilder
{
    public function build(
        RootRenderableInterface $renderable,
        RenderingContext $renderingContext,
    ): ViewModel;

    public function supports(RootRenderableInterface $renderable): bool;
}
Copied!

supports() is called first; return true only for the renderable types your builder handles. build() is then called to produce the view model.

The easiest starting point is to extend \CPSIT\Typo3HandlebarsForms\Domain\ViewModel\Builder\AbstractViewModelBuilder. It takes care of the boilerplate:

  • Wraps the rendering inside a simulated <formvh:renderRenderable> context so EXT:form's state is correct.
  • Applies grid-column classes when the renderable is inside a GridRow.
  • Provides renderAdditionalAttributes() to resolve fluidAdditionalAttributes from the element's properties.

Override renderRenderable() to produce your custom view model; return null from it to fall back to the default ViewHelperContainedViewModel that wraps the full rendered output of the view helper.

Minimal example

use CPSIT\Typo3HandlebarsForms;
use TYPO3\CMS\Fluid;
use TYPO3\CMS\Form;

final class RatingViewModelBuilder extends Typo3HandlebarsForms\Domain\ViewModel\Builder\AbstractViewModelBuilder
{
    protected array $supportedTypes = ['Rating'];

    protected function renderRenderable(
        Form\Domain\Model\Renderable\RootRenderableInterface $renderable,
        Fluid\Core\Rendering\RenderingContext $renderingContext,
    ): ?Typo3HandlebarsForms\Domain\ViewModel\ViewModel {
        $result = $this->viewHelperInvoker->invoke(
            $renderingContext,
            Fluid\ViewHelpers\Form\TextfieldViewHelper::class,
            [
                'type'     => 'number',
                'property' => $renderable->getIdentifier(),
                'id'       => $renderable->getUniqueIdentifier(),
                'additionalAttributes' => $this->renderAdditionalAttributes($renderable, $renderingContext),
            ],
        );

        return new Typo3HandlebarsForms\Domain\ViewModel\ViewHelperContainedViewModel($renderable, $result);
    }
}
Copied!

Registration 

The ViewModelBuilder interface carries a #[AutoconfigureTag('handlebars_forms.view_model_builder')] attribute. Any class that implements the interface is therefore registered automatically via Symfony DI when autowiring is enabled (the default for extensions that include a Configuration/Services.yaml with autowire: true).

No explicit YAML service definition is needed.

Builder priority 

When multiple builders claim the same renderable type via supports(), the first one in the service iterator wins. The iterator order is determined by the Symfony DI priority tag attribute. Built-in builders are registered without an explicit priority (i.e. priority 0).

To ensure your builder runs before a built-in one, set a higher priority:

# Configuration/Services.yaml
Vendor\MyExtension\Domain\ViewModel\Builder\RatingViewModelBuilder:
    tags:
        - name: handlebars_forms.view_model_builder
          priority: 10
Copied!

Choosing a view model type 

Pick the view model type that best matches what your TypoScript configuration will consume:

Type When to use
ViewHelperContainedViewModel Wraps the rendered tag from a Fluid view helper. Use when HBS_TAG and HBS_VH_CONTENT need to read from it.
StandaloneTagViewModel Wraps a manually-built TagBuilder. Use for composite elements such as Fieldset where you construct the tag yourself.
FormFieldViewModel Combines a label (as ViewHelperInvocationResult) with a child view model. Use when HBS_LABEL should return the view-helper-resolved label rather than the raw renderable label.
ViewModelCollection Holds a named map of child view models. Use for multi-part elements such as AdvancedPassword or FileUpload where the template needs to access named children. HBS_CHILDREN iterates over these.
SimpleViewModel Thin wrapper over a renderable with no extra state. Use as a fallback or for elements whose properties are accessed entirely via HBS_PROPERTY.

Custom content objects 

If the built-in HBS_* content objects do not cover a required value, you can create your own by extending \CPSIT\Typo3HandlebarsForms\ContentObject\AbstractHandlebarsFormsContentObject.

Implementing a custom content object 

The abstract base class has one abstract method to implement:

abstract protected function resolve(
    array $configuration,
    ValueResolutionContext $context,
): mixed;
Copied!

$configuration is the TypoScript sub-tree below your content object's key. $context exposes the current state:

  • $context->renderable – the EXT:form renderable being processed
  • $context->viewModel – its view model
  • $context->formRuntime – the active form runtime
  • $context->renderingContext – the Fluid rendering context

Return any value. Strings are returned directly to the TypoScript tree. Non-string values (arrays, objects) are automatically stored in the ValueCollector under a placeholder key; ProcessFormProcessor replaces the placeholder with the real value after the full tree has been resolved.

Example

use CPSIT\Typo3HandlebarsForms\ContentObject;
use Symfony\Component\DependencyInjection;

#[DependencyInjection\Attribute\AutoconfigureTag(
    'frontend.contentobject',
    ['identifier' => 'MY_CUSTOM_COBJ'],
)]
final class MyCustomContentObject extends ContentObject\AbstractHandlebarsFormsContentObject
{
    protected function resolve(
        array $configuration,
        ContentObject\Context\ValueResolutionContext $context,
    ): mixed {
        return $context->renderable->getProperties()[$configuration['key'] ?? ''] ?? null;
    }
}
Copied!

Use it in TypoScript like any built-in content object:

myValue = MY_CUSTOM_COBJ
myValue.key = someProperty
Copied!

stdWrap support 

stdWrap is handled automatically by the base class for every HBS_* and custom content object. If resolve() returns a stringable value, stdWrap is applied directly to it. If the value is non-stringable, stdWrap receives an empty string but the original value is set as the currentValue so TypoScript conditions inside stdWrap can still read it.

Registration 

The #[AutoconfigureTag('frontend.contentobject', ['identifier' => '...'])] attribute is sufficient for registration when the class lives in a directory that is autowired. No explicit YAML service definition is needed.

The identifier must be globally unique across all loaded extensions. Prefixing it with your vendor name (e.g. MYVENDOR_FOO) avoids collisions.

Recursing into the TypoScript tree 

$context->process() re-invokes the ProcessFormProcessor pipeline with an arbitrary TypoScript configuration array. Its primary purpose is to let your content object re-process the current (or a different) renderable under a different configuration block — for example, to apply a sub-section of your own configuration, or to switch the active renderable or view model entirely before the pipeline runs.

protected function resolve(
    array $configuration,
    ContentObject\Context\ValueResolutionContext $context,
): mixed {
    $subConfiguration = $configuration['items.'] ?? [];

    if (!is_array($subConfiguration)) {
        return null;
    }

    return $context->process($subConfiguration);
}
Copied!

To process a different renderable or view model, pass them as named arguments:

$context->process($subConfig, renderable: $otherRenderable, viewModel: $otherViewModel);
Copied!

Context stack 

The ContextStack is a shared singleton (shared: true in DI). It holds the stack of ValueResolutionContext objects pushed by ProcessFormProcessor as it walks the TypoScript tree.

Custom content objects receive the current context as a direct argument to resolve() – injecting ContextStack directly is not necessary and is considered internal API.

Migration 

There are currently no migration steps required.

Contribution guide 

Thanks for considering contributing to this extension! Since it is an open source product, its successful further development depends largely on improving and optimizing it together.

The development of this extension follows the official TYPO3 coding standards. To ensure the stability and cleanliness of the code, various code quality tools are used and most components are covered with test cases. In addition, we use DDEV for local development. Make sure to set it up as described below. For continuous integration, we use GitHub Actions.

Preparation 

# Clone repository
git clone https://github.com/CPS-IT/handlebars-forms.git
cd handlebars-forms

# Install dependencies
composer install
Copied!

Development workflow 

A typical contribution workflow looks like this:

  1. Apply automatic fixes

    Use the following commands to normalize and format the code base:

    # Apply all automatic fixes
    composer fix
    
    # Apply specific fixes
    composer fix:composer
    composer fix:editorconfig
    composer fix:php
    Copied!
  2. Run checks

    Use composer check to run the full code quality pipeline locally. This command bundles dependency analysis, static analysis, coding style checks, and Rector in dry-run mode so that potential refactorings can be reviewed without changing files.

    # Run all checks
    composer check
    
    # Run specific checks
    composer check:deps
    composer check:refactor
    composer check:static
    composer check:style
    
    # Run specific style checks
    composer check:style:composer
    composer check:style:editorconfig
    composer check:style:php
    composer check:style:typoscript
    Copied!
  3. Run refactorings

    Refactorings are intentionally separated from regular checks because they may change the code base.

    # Run all configured refactorings
    composer refactor
    
    # Run specific refactorings
    composer refactor:php
    Copied!
  4. Run tests

    Run the full test suite before opening a pull request:

    # Run all tests
    ddev composer test
    ddev composer test:coverage
    
    # Run functional tests
    ddev composer test:functional
    ddev composer test:functional:coverage
    
    # Run unit tests
    ddev composer test:unit
    ddev composer test:unit:coverage
    
    # Merge coverage reports
    ddev composer test:merge-coverage
    Copied!

Coverage reports 

Code coverage reports are written to Build/tests/coverage. Open the latest merge HTML report with:

open Build/tests/coverage/html/_merged/index.html
Copied!

Pull requests 

Once the changes are ready, please submit a pull request and describe what was changed and why. Ideally, the pull request references an issue that describes the problem being solved.

All documented code quality tools are executed automatically for pull requests across the currently supported PHP versions. For details, refer to the GitHub Actions workflows.

Sitemap