Skip to content
Open
Show file tree
Hide file tree
Changes from 33 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
129 changes: 129 additions & 0 deletions doc/filters/html_attr_merge.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
``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 arrays that represent
HTML attribute values. It is primarily designed for working with arrays
that are passed to the :ref:`html_attr` function.

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 attributes conditionally by merging multiple arrays
based on conditions. Values like ``false``, ``null`` as well as empty arrays are
skipped, making conditional merging convenient:
Copy link
Contributor

Choose a reason for hiding this comment

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

Does it mean one cannot override 'disabled: true' with later value ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Need to improve this wording obviously.

It means that in the preceding example, e. g. variant2 could be false, null or an empty array and would be skipped then.

If one of these values itself is an array and contains a key-value pair with a false or null value, that will be used to override...

{ disabled: true }|html_attr_merge({ disabled: false }) will be { disabled: false }.


.. 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']},
Copy link
Contributor

Choose a reason for hiding this comment

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

Is class required to be an array ? If no, i think we should have at least one example with a string value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, it's not. But...

{ class: 'foo' }|html_attr_merge({ class: 'bar' }) will be { class: 'bar' }, because simple scalar merge semantics apply.

This is what I meant in my initial description with users having to be consistent and disciplined about using either scalars (strings) or arrays.

For attributes where multiple values make sense, users should use arrays. We don't want to know which attributes those are, so we burden the users with knowing the attribute semantics and being consistent about it.

Makes sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe 230bcf7 clarifies it.

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

**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'}} #}

**``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 arrays to merge. Each argument can be:

* An array or iterable 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`
124 changes: 124 additions & 0 deletions doc/filters/html_attr_type.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
``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 associative arrays (property-value
pairs) and numeric arrays (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 array or iterable 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