Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b3bf116
Add an `attr` function to make outputting HTML attributes easier
mpdude Dec 6, 2023
d0b46d7
Support boolean attribute values
mpdude Sep 30, 2024
987fbd2
Add a first test POC
mpdude Oct 1, 2024
0b14080
Add tests for key merging
mpdude Oct 7, 2024
10ce9c6
Use `[... ]` instead of `array_merge`#
mpdude Oct 7, 2024
5ca5ac1
Update extra/html-extra/HtmlExtension.php
mpdude Oct 7, 2024
04e8549
Fix CS
mpdude Oct 7, 2024
9b1cb14
Add initial functional test for the `html_attr_merge` filter
mpdude Oct 7, 2024
5be60b3
Rename variable, consider option of having named parameters in variad…
mpdude Jan 2, 2026
65651a8
Rework rules for merging and printing attributes
mpdude Jan 2, 2026
53abfba
Improve handling of simple "style" attributes
mpdude Jan 2, 2026
9c21487
Apply fabbot CS fixes
mpdude Jan 2, 2026
1786118
Introduce AttributeValueInterface to deal with different attribute types
mpdude Jan 8, 2026
bad3a71
Use segregated interfaces; remove extension of \ArrayExtension, which…
mpdude Jan 9, 2026
6b4a02b
Avoid disabling auto-escape in tests
mpdude Jan 9, 2026
29a7032
Use a type hint for variadic argument
mpdude Jan 9, 2026
dc01c02
Add docblocks describing the interfaces
mpdude Jan 9, 2026
a453ca2
Apply FabBot CS fixes
mpdude Jan 9, 2026
fefbaf2
Fix a test
mpdude Jan 9, 2026
5e7bf9c
Add documentation (mostly written by Claude Code, with human directio…
mpdude Jan 9, 2026
38c7d3e
Mention conversion for `true` in `data-*`
mpdude Jan 9, 2026
8ec8bfb
Address GH review feedback regarding documentation
mpdude Jan 9, 2026
f6ff09d
Rename "MergeInterface" to "MergeableInterface"
mpdude Jan 9, 2026
56db058
Add type checks, hints and docblocks
mpdude Jan 9, 2026
06c96c8
Add a short ternary example to the tests
mpdude Jan 9, 2026
830be2c
Fix argument type hinting
mpdude Jan 9, 2026
6254232
Add variadic argument example in the html_attr_merge.test
mpdude Jan 9, 2026
bf33bdf
Fixup CS
mpdude Jan 9, 2026
62b2584
Fix a typo
mpdude Jan 9, 2026
ca4ad98
Add PHPUnit tests for various html_attr input scenarios
mpdude Jan 9, 2026
2668577
Add PHPUnit test cases for merge behavior
mpdude Jan 9, 2026
3aee494
Apply CS fixes
mpdude Jan 9, 2026
0f82b04
Add a test showcasing passing objects to html_attr
mpdude Jan 10, 2026
b3d62dc
Apply documentation rewording suggestions from code review
mpdude Jan 11, 2026
fbb0bb5
Record co-authorship
polarbirke Jan 11, 2026
a80bdb4
Improve documentation in a few places as suggested in GH review comments
mpdude Jan 11, 2026
bd41ee6
Fix MergeableInterface return types and explanation
mpdude Jan 12, 2026
e8c36bf
Clarify `style` attribute behavior in docs
mpdude Jan 12, 2026
230bcf7
Add a note when to use scalars and when to use arrays in attribute va…
mpdude Jan 12, 2026
352e564
Add a CHANGELOG entry
mpdude Jan 12, 2026
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
4 changes: 2 additions & 2 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 3.22.2 (2026-XX-XX)
# 3.23.0 (2026-XX-XX)

* n/a
* Add the `html_attr` function and `html_attr_merge` as well as `html_attr_type` filters

# 3.22.2 (2025-12-14)

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

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

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

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.

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

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

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