Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Add support for renaming variables in object destructuring (`{name: userName} = user`)
* Add `html_attr_relaxed` escaping strategy that preserves :, @, [, and ] for front-end framework attribute names
* Add support for short-circuiting in null-safe operator chains
* Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters

# 3.23.0 (2026-01-23)

Expand Down
141 changes: 141 additions & 0 deletions doc/filters/html_attr_merge.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
``html_attr_merge``
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation for those functions and filters should include the note about the fact that they are part of an extra extension, not of Twig core (see the documentation of the html_classes function or the data_uri filter for existing usages)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I see that it is actually present inside the section about merging rules. This is confusing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's always at the end of the documentation pages, like for example with html_cva – those other pages also have other preceding sub-headlines.

===================

.. _html_attr_merge:

.. versionadded:: 3.24

The ``html_attr_merge`` filter was added in Twig 3.24.

The ``html_attr_merge`` filter merges multiple mappings that represent
HTML attribute values. Such mappings contain the names of the HTML attributes
as keys, and the corresponding values represent the attributes' values.

It is primarily designed for working with arrays that are passed to the
:ref:`html_attr` function. It closely resembles the :doc:`merge <../filters/merge>`
filter, but has different merge behavior for values that are iterables
themselves, as it will merge such values in turn.

The filter returns a new merged array:

.. code-block:: twig

{% set base = {class: ['btn'], type: 'button'} %}
{% set variant = {class: ['btn-primary'], disabled: true} %}

{% set merged = base|html_attr_merge(variant) %}

{# merged is now: {
class: ['btn', 'btn-primary'],
type: 'button',
disabled: true
} #}

The filter accepts multiple arrays as arguments and merges them from left to right:

.. code-block:: twig

{% set merged = base|html_attr_merge(variant1, variant2, variant3) %}

A common use case is to build attribute mappings conditionally by merging multiple
parts based on conditions. To make this conditional merging more convenient, filter
arguments that are ``false``, ``null`` or empty arrays are ignored:

.. code-block:: twig

{% set button_attrs = {
type: 'button',
class: ['btn']
}|html_attr_merge(
variant == 'primary' ? { class: ['btn-primary'] },
variant == 'secondary' ? { class: ['btn-secondary'] },
size == 'large' ? { class: ['btn-lg'] },
size == 'small' ? { class: ['btn-sm'] },
disabled ? { disabled: true, class: ['btn-disabled'] },
loading ? { 'aria-busy': 'true', class: ['btn-loading'] },
) %}

{# Example with variant='primary', size='large', disabled=false, loading=true:

The false values (secondary variant, small size, disabled state) are ignored.

button_attrs is:
{
type: 'button',
class: ['btn', 'btn-primary', 'btn-lg', 'btn-loading'],
'aria-busy': 'true'
}
#}

Merging Rules
Comment thread
mpdude marked this conversation as resolved.
-------------

The filter follows these rules when merging attribute values:

**Scalar values**: Later values override earlier ones.

.. code-block:: twig

{% set result = {id: 'old'}|html_attr_merge({id: 'new'}) %}
{# result: {id: 'new'} #}

**Array values**: Arrays are merged like in PHP's ``array_merge`` function - numeric keys are
appended, non-numeric keys replace.
Comment thread
mpdude marked this conversation as resolved.

.. code-block:: twig

{# Numeric keys (appended): #}
{% set result = {class: ['btn']}|html_attr_merge({class: ['btn-primary']}) %}
{# result: {class: ['btn', 'btn-primary']} #}

{# Non-numeric keys (replaced): #}
{% set result = {class: {base: 'btn', size: 'small'}}|html_attr_merge({class: {variant: 'primary', size: 'large'}}) %}
{# result: {class: {base: 'btn', size: 'large', variant: 'primary'}} #}

.. note::

Remember, attribute mappings passed to or returned from this filter are regular
Twig mappings after all. If you want to completely replace an attribute value
that is an iterable with another value, you can use the :doc:`merge <../filters/merge>`
filter to do that.

**``MergeableInterface`` implementations**: For advanced use cases, attribute values can be objects
that implement the ``MergeableInterface``. These objects can define their own, custom merge
behavior that takes precedence over the default rules. See the docblocks in that interface
for details.

.. note::

The ``html_attr_merge`` filter is part of the ``HtmlExtension`` which is not
installed by default. Install it first:

.. code-block:: bash

$ composer require twig/html-extra

Then, on Symfony projects, install the ``twig/extra-bundle``:

.. code-block:: bash

$ composer require twig/extra-bundle

Otherwise, add the extension explicitly on the Twig environment::

use Twig\Extra\Html\HtmlExtension;

$twig = new \Twig\Environment(...);
$twig->addExtension(new HtmlExtension());

Arguments
---------

The filter accepts a variadic list of arguments to merge. Each argument can be:

* A map of attributes
* ``false`` or ``null`` (ignored, useful for conditional merging)
* An empty string ``''`` (ignored, to support implicit else in ternary operators)

.. seealso::

:ref:`html_attr`,
:doc:`html_attr_type`
123 changes: 123 additions & 0 deletions doc/filters/html_attr_type.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
``html_attr_type``
==================

.. _html_attr_type:

.. versionadded:: 3.24

The ``html_attr_type`` filter was added in Twig 3.24.

The ``html_attr_type`` filter converts arrays into specialized attribute value
objects that implement custom rendering logic. It is designed for use
with the :ref:`html_attr` function for attributes where
the attribute value follows special formatting rules.

.. code-block:: html+twig

<img {{ html_attr({
srcset: ['small.jpg 480w', 'large.jpg 1200w']|html_attr_type('cst')
}) }}>

{# Output: <img srcset="small.jpg 480w, large.jpg 1200w"> #}

Available Types
---------------

Space-Separated Token List (``sst``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Used for attributes that expect space-separated values, like ``class`` or
``aria-labelledby``:

.. code-block:: html+twig

{% set classes = ['btn', 'btn-primary']|html_attr_type('sst') %}

<button {{ html_attr({class: classes}) }}>
Click me
</button>

{# Output: <button class="btn btn-primary">Click me</button> #}

This is the default type used when the :ref:`html_attr` function encounters an
array value (except for ``style`` attributes).

Comma-Separated Token List (``cst``)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Used for attributes that expect comma-separated values, like ``srcset`` or
``sizes``:

.. code-block:: html+twig

<img {{ html_attr({
srcset: ['image-1x.jpg 1x', 'image-2x.jpg 2x', 'image-3x.jpg 3x']|html_attr_type('cst'),
sizes: ['(max-width: 600px) 100vw', '50vw']|html_attr_type('cst')
}) }}>

{# Output: <img srcset="image-1x.jpg 1x, image-2x.jpg 2x, image-3x.jpg 3x" sizes="(max-width: 600px) 100vw, 50vw"> #}

Inline Style (``style``)
~~~~~~~~~~~~~~~~~~~~~~~~

Used for style attributes. Handles both maps (property - value pairs) and sequences (CSS declarations):

.. code-block:: html+twig

{# Associative array #}
{% set styles = {color: 'red', 'font-size': '14px'}|html_attr_type('style') %}

<div {{ html_attr({style: styles}) }}>
Styled content
</div>

{# Output: <div style="color: red; font-size: 14px;">Styled content</div> #}

{# Numeric array #}
{% set styles = ['color: red', 'font-size: 14px']|html_attr_type('style') %}

<div {{ html_attr({style: styles}) }}>
Styled content
</div>

{# Output: <div style="color: red; font-size: 14px;">Styled content</div> #}

The ``style`` type is automatically applied by the :ref:`html_attr` function when
it encounters an array value for the ``style`` attribute.
Comment on lines +85 to +86
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would have been the case for both previous example, no? I mean, what would happen to both examples without the |html_attr_type ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it would, because of the default handling of attributes named style with this type.

TBH I am not sure if this type should be exposed in the first place.

WDYT – do you see any situation where one might want to have inline CSS in another attribute? That could be used to improve this section.


.. note::

The ``html_attr_type`` filter is part of the ``HtmlExtension`` which is not
installed by default. Install it first:

.. code-block:: bash

$ composer require twig/html-extra

Then, on Symfony projects, install the ``twig/extra-bundle``:

.. code-block:: bash

$ composer require twig/extra-bundle

Otherwise, add the extension explicitly on the Twig environment::

use Twig\Extra\Html\HtmlExtension;

$twig = new \Twig\Environment(...);
$twig->addExtension(new HtmlExtension());

Arguments
---------

* ``value``: The sequence of attributes to convert
* ``type``: The attribute type. One of:

* ``sst`` (default): Space-separated token list
* ``cst``: Comma-separated token list
* ``style``: Inline CSS styles

.. seealso::

:ref:`html_attr`,
:ref:`html_attr_merge`
2 changes: 2 additions & 0 deletions doc/filters/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ Filters
format_datetime
format_number
format_time
html_attr_merge
html_attr_type
html_to_markdown
inline_css
inky_to_html
Expand Down
Loading
Loading