loading...
Cover image for The Power Of Twig's Merge Filter With Drupal 8 Templates

The Power Of Twig's Merge Filter With Drupal 8 Templates

jonmcl profile image Jon McLaughlin 🍄 ・4 min read

Rendering web content in Drupal is a little bit like building a web page that is an onion. You have to peel back the layers to find the right place to make changes. Luckily, most everything ends up at a suggested Twig template. The Twig template engine is one of the best things about Drupal 8. It is powerful and easy to use for all levels of developer proficiencies.

I often find myself creating Twig templates for different node content types and view modes. So, for example, I might have a Twig template in my theme that is named "node--project--full.html.twig". This template will get utilized for the "project" content types and when the "full" view mode is used.

This method allows me to put one field of the project content type in a specific place on the page. Maybe below it will be more fields that are in a 9 column space on the left, and a few more fields are in a 3 column space on the right.

Twig lets you use regular HTML to create the layout for this content type & view mode. You can then use simple Twig syntax to place various fields in various places of the layout.

<h1{{ title_attributes }}>{{ label }}</h1>
<div class="row">
    <div class="columns medium-9">
        {{ content.body }}
    </div>
    <div class="columns medium-3">
        {{ content.field_project_date }}
        {{ content.field_project_link }}
        {{ content.field_tags }}
    </div>
</div>

Very simple and works well, but what if you want to customize the rendered output that comes from each of those content.field elements? Drupal and Twig definitely allow you to peel back the onion and use a usage specific templates for each of those field elements. For example, you can put a Twig template named "field--node--field-project-date--project--full.html.twig" in your theme, rebuild the Drupal cache, and then you can get as specific as you want for the rendered HTML of that particular field when it is used in that particular content type ("project") and for that particular view mode ("full").

I use this pattern for simple changes to the fields. Maybe I want to add a "button" class to a link field when it is used in a specific view mode. Or maybe I want to use a specific field label. Drupal's pattern of template suggestions works well, but in the end you do end up with potentially dozens of field templates in your theme.

For simple customization of individually fields, particularly when the customization only applies to a node & view template, I realized there is a way to use Twig's merge filter.

Twig filters are simple utility functions that you can call on any variable that is in the Twig template's context.

Let's say I need to use a different label for a field in one particular view mode. Instead of creating a specific field template, I can just alter the node's template like so:

<h1{{ title_attributes }}>{{ label }}</h1>
<div class="row">
    <div class="columns medium-9">
      {{ content.body }}
    </div>
    <div class="columns medium-3">
      {{ content.field_project_date|merge({
        '#label_display': 'above',
        '#title': 'Year',
      }) }}
      {{ content.field_project_link }}
      {{ content.field_tags }}
    </div>
</div>

At this level of Drupal's render onion, content is an array of fields and each field is another array.

In this case, I'm changing the field label from "Project Launch Date" to simply "Year". I will still have to customize the output of the field formatter to just display a year, but most Drupal field formatters don't allow for label customization beyond showing or hiding it.

Most standard Drupal field formatters also don't allow for customization of the class attributes that get used when the field is rendered. You could create separate field templates or you could use various preprocess hooks in your front-end theme. A possibly simpler way is to just use the merge filter in your node Twig template:

    {{ content.field_project_link|merge({
      '#attributes': { 'class': ['my-custom-class', 'another-custom-class'] },
    }) }}

It is worth noting that these additional class attributes are being applied to the field wrapper. If you want to apply changes to the individual items of the field this would likely not be possible to do at this level of the onion. You might still need to create a "field--field-project-link.html.twig" template, but you could maybe avoid having to write a preprocess function for it if you just wanted to add some additional attributes to the link items:

  <div{{ attributes.addClass(classes) }}>
    {% for item in items %}
      {{ item.content|merge({
        '#attributes': {'class': ['button', 'primary']}
      }) }}
    {% endfor %}
  </div>

To do any of this you will have to have a good understanding of all the various Drupal render arrays that your fields might be using. One of the best ways to learn what is going on inside of a Twig template is to configure your IDE to allow you to place debug breakpoints within the template itself. Then you can just inspect the $context variable and see which theme function is being called when something like item.content is rendered.

I'd love to hear if you think this method makes things simpler or more complicated.

Discussion

pic
Editor guide