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.
Comment | File | Size | Author |
---|---|---|---|
#19 | 3081314-19.patch | 9.79 KB | JohnAlbin |
|
Comments
Comment #3
JohnAlbinComment #4
JohnAlbinComment #5
JohnAlbinComment #6
JohnAlbinComment #7
JohnAlbinWhile 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.
Comment #8
JohnAlbinI've been thinking about the
{{ form|set( ['email', 'attributes', 'placeholder'], 'Your email'|t ) }}
filter. Twig'smerge()
filter uses PHP'sarray_merge()
function underneath. PHP also has aarray_merge_recursive()
andarray_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 areplace()
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 theoreticallyreplace()
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 aset()
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.Comment #9
JohnAlbinComment #10
JohnAlbinComment #11
JohnAlbinComment #12
JohnAlbinUpdated patch.
Comment #13
JohnAlbinUpdated proposed syntax.
Comment #14
JohnAlbinComment #15
JohnAlbinComment #16
JohnAlbinComment #17
JohnAlbinWrong patch.
Comment #18
JohnAlbinComment #19
JohnAlbinComment #21
JohnAlbinComment #23
markconroy CreditAttribution: markconroy at Annertech commentedHi 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?