Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c53c6ab
Add an `attr` function to make outputting HTML attributes easier
mpdude Dec 6, 2023
b338e23
Support boolean attribute values
mpdude Sep 30, 2024
692ce55
Add a first test POC
mpdude Oct 1, 2024
6fe5e50
Add tests for key merging
mpdude Oct 7, 2024
2d0166b
Use `[... ]` instead of `array_merge`#
mpdude Oct 7, 2024
e49ac44
Update extra/html-extra/HtmlExtension.php
mpdude Oct 7, 2024
97825a8
Fix CS
mpdude Oct 7, 2024
0e001ef
Add initial functional test for the `html_attr_merge` filter
mpdude Oct 7, 2024
4a9bddf
Rename variable, consider option of having named parameters in variad…
mpdude Jan 2, 2026
3ffa15e
Rework rules for merging and printing attributes
mpdude Jan 2, 2026
80449f8
Improve handling of simple "style" attributes
mpdude Jan 2, 2026
65b627f
Apply fabbot CS fixes
mpdude Jan 2, 2026
22da58f
Introduce AttributeValueInterface to deal with different attribute types
mpdude Jan 8, 2026
71b2a9e
Use segregated interfaces; remove extension of \ArrayExtension, which…
mpdude Jan 9, 2026
44a0e97
Avoid disabling auto-escape in tests
mpdude Jan 9, 2026
211d063
Use a type hint for variadic argument
mpdude Jan 9, 2026
5feb8be
Add docblocks describing the interfaces
mpdude Jan 9, 2026
c0e8efb
Apply FabBot CS fixes
mpdude Jan 9, 2026
7d468f5
Fix a test
mpdude Jan 9, 2026
fd8fd1d
Add documentation (mostly written by Claude Code, with human directio…
mpdude Jan 9, 2026
766e4c0
Mention conversion for `true` in `data-*`
mpdude Jan 9, 2026
d159cbc
Address GH review feedback regarding documentation
mpdude Jan 9, 2026
461ac3a
Rename "MergeInterface" to "MergeableInterface"
mpdude Jan 9, 2026
535aaaa
Add type checks, hints and docblocks
mpdude Jan 9, 2026
28ab29e
Add a short ternary example to the tests
mpdude Jan 9, 2026
78cf54b
Fix argument type hinting
mpdude Jan 9, 2026
e7f2332
Add variadic argument example in the html_attr_merge.test
mpdude Jan 9, 2026
125f286
Fixup CS
mpdude Jan 9, 2026
c534e01
Fix a typo
mpdude Jan 9, 2026
a52d4ae
Add PHPUnit tests for various html_attr input scenarios
mpdude Jan 9, 2026
caa0ac4
Add PHPUnit test cases for merge behavior
mpdude Jan 9, 2026
2d64cc2
Apply CS fixes
mpdude Jan 9, 2026
a8d2c49
Add a test showcasing passing objects to html_attr
mpdude Jan 10, 2026
49fb7e9
Apply documentation rewording suggestions from code review
mpdude Jan 11, 2026
6d054a3
Record co-authorship
polarbirke Jan 11, 2026
d8a105f
Improve documentation in a few places as suggested in GH review comments
mpdude Jan 11, 2026
85dbac7
Fix MergeableInterface return types and explanation
mpdude Jan 12, 2026
42ccdf5
Clarify `style` attribute behavior in docs
mpdude Jan 12, 2026
d6c6d8f
Add a note when to use scalars and when to use arrays in attribute va…
mpdude Jan 12, 2026
5d30e22
Add a CHANGELOG entry
mpdude Jan 12, 2026
b452159
Omit escaping of attribute names
mpdude Jan 20, 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
3 changes: 2 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# 3.22.2 (2026-XX-XX)
# 3.23.0 (2026-XX-XX)

* Add `===` and `!==` operators (equivalent to the `same as` and `not same as` tests)
* 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
Loading