---
title: "Custom view model builders"
manual: "Handlebars"
version: "main"
permalink: "https://docs.typo3.org/permalink/cpsit/typo3-handlebars-forms:custom-view-model-builders@main"
source: "DeveloperCorner/ViewModelBuilders.rst"
rendered: "2026-09-29T09:18:02+00:00"
---

# Custom view model builders {#custom-view-model-builders}

Every EXT:form renderable that reaches `HBS_RENDERABLES` is converted to a
[view model](https://docs.typo3.org/permalink/cpsit/typo3-handlebars-forms:view-models@main) 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 {#custom-vmb-implement}

The `ViewModelBuilder` interface declares two methods:

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

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

`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**

```php
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);
    }
}
```

## Registration {#custom-vmb-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
autoconfiguration is enabled (the default for extensions that include a
`Configuration/Services.yaml` with `autoconfigure: true`).

No explicit YAML service definition is needed.

## Builder priority {#custom-vmb-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](https://symfony.com/doc/current/service_container/tags.html#tagged-services-with-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 using the
`#[AsTaggedItem]` attribute:

```php
use CPSIT\Typo3HandlebarsForms;
use Symfony\Component\DependencyInjection;

#[DependencyInjection\Attribute\AsTaggedItem(priority: 10)]
final class RatingViewModelBuilder extends Typo3HandlebarsForms\Domain\ViewModel\Builder\AbstractViewModelBuilder
{
    // ...
}
```

Like `#[AutoconfigureTag]`, the attribute is only evaluated when autoconfiguration
is enabled. Alternatively, e.g. if autoconfiguration is disabled or you cannot modify
the builder class, set the priority in your `Configuration/Services.yaml`:

**EXT:my_extension/Configuration/Services.yaml**

```yaml
services:
  Vendor\MyExtension\Domain\ViewModel\Builder\RatingViewModelBuilder:
    tags:
      - name: handlebars_forms.view_model_builder
        priority: 10
```

## Choosing a view model type {#custom-vmb-view-model-types}

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`. |
