Description
I'm working on integrating a form theme for a custom kit based on Symfony UX Toolkit. This form theme uses Twig Components (<twig:> syntax) instead of plain HTML tags.
The problem
In the default form theme (form_div_layout.html.twig), blocks like widget_attributes and attributes render HTML attribute strings:
<input type="text" {{ block('widget_attributes') }} />
{# outputs: id="name" name="form[name]" required="required" #}
The <twig:> spread syntax ({{ ...hash }}) works with Twig Components but requires an associative array, not an HTML string. Today, there is no built-in way to get the processed attributes (with translations applied and booleans normalized) as a hash.
Solution
Instead of adding new PHP functions that would duplicate the logic already in the Twig blocks (translations, boolean normalization), the approach leverages a new Twig core feature: block_data() and block_export() (see twigphp/Twig#4919, PR twigphp/Twig#4920).
How it works:
-
The attributes block in form_div_layout.html.twig is modified to call block_export(attrs) after building the normalized attributes hash — exporting the structured data via a side-channel while still rendering the HTML string as before.
-
A Twig Component form theme can then use block_data('widget_attributes') to get the attributes as a hash:
{%- block form_widget_simple -%}
<twig:Input type="{{ type }}" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} />
{%- endblock form_widget_simple -%}
Changes to form_div_layout.html.twig:
The widget_attributes, widget_container_attributes, and button_attributes blocks set attr with proper ordering (standard attributes first, user attributes merged after):
{%- block widget_attributes -%}
{%- set attr = {id: id, name: full_name, disabled: disabled, required: required}|merge(attr) -%}
{{- block('attributes') -}}
{%- endblock widget_attributes -%}
{%- block widget_container_attributes -%}
{%- if id is not empty -%}
{%- set attr = {id: id}|merge(attr) -%}
{%- endif -%}
{{- block('attributes') -}}
{%- endblock widget_container_attributes -%}
{%- block button_attributes -%}
{%- set attr = {id: id, name: full_name, disabled: disabled}|merge(attr) -%}
{{- block('attributes') -}}
{%- endblock button_attributes -%}
The attributes block now builds and exports a hash, then renders it as HTML:
{% block attributes -%}
{%- set attrs = {} -%}
{%- for attrname, attrvalue in attr -%}
{%- if attrname in ['placeholder', 'title'] -%}
{%- set attrs = attrs|merge({(attrname): translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain)}) -%}
{%- elseif attrvalue is same as(true) -%}
{%- set attrs = attrs|merge({(attrname): attrname}) -%}
{%- elseif attrvalue is not same as(false) -%}
{%- set attrs = attrs|merge({(attrname): attrvalue}) -%}
{%- endif -%}
{%- endfor -%}
{%- do block_export(attrs) -%}
{%- for attrname, attrvalue in attrs -%}
{{- ' ' ~ attrname }}="{{ attrvalue }}"
{%- endfor -%}
{%- endblock attributes -%}
Key benefits:
- No logic duplication: The attribute processing logic stays in the Twig blocks, benefiting from block inheritance and overrides
- Fully backward-compatible: Existing form themes continue to work unchanged —
block('widget_attributes') still renders HTML
- Clean integration: Twig Component form themes use
block_data() for spread syntax, getting a proper hash
Example Twig Component form theme:
{%- block form_widget_simple -%}
<twig:Input type="{{ type }}" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} />
{%- endblock form_widget_simple -%}
{%- block checkbox_widget -%}
<twig:Checkbox :checked="checked" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} />
{%- endblock checkbox_widget -%}
{%- block button_widget -%}
<twig:Button type="{{ type|default('button') }}" {{ ...block_data('button_attributes') }}>
{{- block(outerBlocks.form_label_content) -}}
</twig:Button>
{%- endblock button_widget -%}
{%- block form_row_render -%}
{%- set attr = row_attr -%}
<twig:Fieldset:Field {{ ...block_data('attributes') }}>
{{- form_label(form) -}}
{{- form_help(form) -}}
{{- form_widget(form, widget_attr) -}}
{{- form_errors(form) -}}
</twig:Fieldset:Field>
{%- endblock form_row_render -%}
Dependencies
This depends on twigphp/Twig#4919 (PR twigphp/Twig#4920) which adds block_data() and block_export() to Twig core.
Description
I'm working on integrating a form theme for a custom kit based on Symfony UX Toolkit. This form theme uses Twig Components (
<twig:>syntax) instead of plain HTML tags.The problem
In the default form theme (form_div_layout.html.twig), blocks like
widget_attributesandattributesrender HTML attribute strings:The
<twig:>spread syntax ({{ ...hash }}) works with Twig Components but requires an associative array, not an HTML string. Today, there is no built-in way to get the processed attributes (with translations applied and booleans normalized) as a hash.Solution
Instead of adding new PHP functions that would duplicate the logic already in the Twig blocks (translations, boolean normalization), the approach leverages a new Twig core feature:
block_data()andblock_export()(see twigphp/Twig#4919, PR twigphp/Twig#4920).How it works:
The
attributesblock inform_div_layout.html.twigis modified to callblock_export(attrs)after building the normalized attributes hash — exporting the structured data via a side-channel while still rendering the HTML string as before.A Twig Component form theme can then use
block_data('widget_attributes')to get the attributes as a hash:{%- block form_widget_simple -%} <twig:Input type="{{ type }}" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} /> {%- endblock form_widget_simple -%}Changes to
form_div_layout.html.twig:The
widget_attributes,widget_container_attributes, andbutton_attributesblocks setattrwith proper ordering (standard attributes first, user attributes merged after):{%- block widget_attributes -%} {%- set attr = {id: id, name: full_name, disabled: disabled, required: required}|merge(attr) -%} {{- block('attributes') -}} {%- endblock widget_attributes -%} {%- block widget_container_attributes -%} {%- if id is not empty -%} {%- set attr = {id: id}|merge(attr) -%} {%- endif -%} {{- block('attributes') -}} {%- endblock widget_container_attributes -%} {%- block button_attributes -%} {%- set attr = {id: id, name: full_name, disabled: disabled}|merge(attr) -%} {{- block('attributes') -}} {%- endblock button_attributes -%}The
attributesblock now builds and exports a hash, then renders it as HTML:{% block attributes -%} {%- set attrs = {} -%} {%- for attrname, attrvalue in attr -%} {%- if attrname in ['placeholder', 'title'] -%} {%- set attrs = attrs|merge({(attrname): translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain)}) -%} {%- elseif attrvalue is same as(true) -%} {%- set attrs = attrs|merge({(attrname): attrname}) -%} {%- elseif attrvalue is not same as(false) -%} {%- set attrs = attrs|merge({(attrname): attrvalue}) -%} {%- endif -%} {%- endfor -%} {%- do block_export(attrs) -%} {%- for attrname, attrvalue in attrs -%} {{- ' ' ~ attrname }}="{{ attrvalue }}" {%- endfor -%} {%- endblock attributes -%}Key benefits:
block('widget_attributes')still renders HTMLblock_data()for spread syntax, getting a proper hashExample Twig Component form theme:
{%- block form_widget_simple -%} <twig:Input type="{{ type }}" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} /> {%- endblock form_widget_simple -%} {%- block checkbox_widget -%} <twig:Checkbox :checked="checked" {{ ...block_data('widget_attributes')|merge({value: value|default}) }} /> {%- endblock checkbox_widget -%} {%- block button_widget -%} <twig:Button type="{{ type|default('button') }}" {{ ...block_data('button_attributes') }}> {{- block(outerBlocks.form_label_content) -}} </twig:Button> {%- endblock button_widget -%} {%- block form_row_render -%} {%- set attr = row_attr -%} <twig:Fieldset:Field {{ ...block_data('attributes') }}> {{- form_label(form) -}} {{- form_help(form) -}} {{- form_widget(form, widget_attr) -}} {{- form_errors(form) -}} </twig:Fieldset:Field> {%- endblock form_row_render -%}Dependencies
This depends on twigphp/Twig#4919 (PR twigphp/Twig#4920) which adds
block_data()andblock_export()to Twig core.