Problem/Motivation

It's currently not possible to set a deeply nested property when printing a Twig variable in a template.

In other words, these don't work:

{# syntax error for "set" #}
{% set submit.attributes.class = 'fancy-button' %}

{# added class isn't printed out. #}
{% do submit.attributes.addClass('fancy-button)' %}
{{ submit }}

{# submit.attributes is completely replaced by the new array #}
{{ submit|merge({ 'attributes': { 'class': [ 'fancy-button' ] } }) }}

Mark Drummond showed a non-obvious way to do this using already existing Twig syntax:

{% set email_item = form.email %}
{% set email_attributes = email_item['#attributes'] %}
{% set email_class = email_attributes['class'] %}
{% set email_class = email_class|merge([ 'new-class' ]) %}
{% set email_attributes = email_attributes|merge({ 'class': email_class }) %}

Proposed resolution

Add two Twig filters to this module, set() and add().

We use array_replace_recursive() as the engine for a set() filter, meaning the syntax would look like this:

{{ form|set( { 'email': { 'attributes': { 'placeholder': 'Your email'|t }}} ) }}

That would use array_replace_recursive() to merge the form variable with the provided array and then prints form.

And if you wanted to add some classes you'd need to merge them into the 'class' array before set()ing them. Like so:

{{ form|set( { 'email': { 'attributes': { 'class': form.email.attributes.class|merge(['new-class']) }}} ) }}

While this set() is flexible (you can alter the render array in multiple ways with one call), it's a little awkward when just wanting to set one deeply nested value. I'm thinking we could add an additional filter that looks like this:

{{ form|add( 'email.attributes.class', 'new-class' ) }}

That add() filter would append 'new-class' into the existing form.email.attributes.class array and then print form.

Original proposed resolution

It sure would be nice if we could something simpler. Maybe something like this:

{{ form|addClass(['email', 'attributes'], 'new-class') }}

{{ form|set( ['email', 'attributes', 'placeholder'], 'Your email'|t ) }}

Improved syntax would be welcome.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

JohnAlbin created an issue. See original summary.

JohnAlbin’s picture

JohnAlbin’s picture

Issue summary: View changes
JohnAlbin’s picture

Issue summary: View changes
JohnAlbin’s picture

Issue summary: View changes
JohnAlbin’s picture

Project: Formdazzle! » Components
Assigned: Unassigned » JohnAlbin

While I thought of this feature while working on formdazzle, its not really specific to forms. It's specific to render arrays. And the components module already has a Twig filter in it. I think it makes sense to put it in that module. Feel free to offer your own opinion on the proper home for this feature.

JohnAlbin’s picture

Title: Add Twig filter to set/call deeply nested properties/methods » Add Twig filters to set deeply nested properties
Version: 8.x-1.x-dev »

I've been thinking about the {{ form|set( ['email', 'attributes', 'placeholder'], 'Your email'|t ) }} filter. Twig's merge() filter uses PHP's array_merge() function underneath. PHP also has a array_merge_recursive() and array_replace_recursive(), the latter of which is better suited to render arrays.

If we use array_replace_recursive() as the engine for a Drupal filter, we could create a replace() filter.

This might make the filter naming confusing though unless you know how the PHP functions work, which most people won't know.

Since the merge() filter does a shallow merge, it actually replaces whole chunks of the render array when you try to merge a property that is deeply nested. While this theoretically replace() filter would merge the render arrays together. *sigh* [edit: oh! And Twig already has a replace() filter that does something else anyway.]

How about we use array_replace_recursive() as the engine for a set() filter? That means the syntax would look like this:

{{ form|set( { 'email': { 'attributes': { 'placeholder': 'Your email'|t }}} ) }}

Also, note that it would merge any existing numeric-index arrays oddly. If the existing 'class' array was ['old-class1', 'old-class2'] and you tried to set it with ['new-class1'], you'd replace the 0 index for the class array and would end up with ['new-class1', 'old-class2']

So if you wanted to add some classes you'd need to merge them into the 'class' array before set()ing them. Like so:

{{ form|set( { 'email': { 'attributes': { 'class': form.email.attributes.class|merge(['new-class']) }}} ) }}

While this set() is flexible, it's a little awkward when just wanting to set one deeply nested value. I'm thinking we could add an additional filter that looks like this:

{{ form|add( 'email.attributes.class', 'new-class' ) }}

And that add() filter would append 'new-class' into the existing form.email.attributes.class array.

JohnAlbin’s picture

Status: Active » Needs review
FileSize
2.8 KB
JohnAlbin’s picture

Project: Components » Components!
Version: » 8.x-1.x-dev
JohnAlbin’s picture

Version: 8.x-1.x-dev » 8.x-2.x-dev
Issue summary: View changes
JohnAlbin’s picture

FileSize
3.14 KB
1.65 KB

Updated patch.

JohnAlbin’s picture

Issue summary: View changes

Updated proposed syntax.

JohnAlbin’s picture

Issue summary: View changes
JohnAlbin’s picture

Issue summary: View changes
JohnAlbin’s picture

FileSize
11.25 KB
JohnAlbin’s picture

Wrong patch.

JohnAlbin’s picture

JohnAlbin’s picture

FileSize
9.79 KB

  • JohnAlbin committed d89768e on 8.x-2.x
    Issue #3081314 by JohnAlbin, mdrummond: Add Twig filters to set deeply...
JohnAlbin’s picture

Status: Needs review » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

markconroy’s picture

Hi John,

This is an interesting looking feature that I've just now come across.

I wonder would it be better placed in a module such as Twig Tweak rather than the components module?