Extendable Field Templates in Drupal 8

Using Twig to create a more concise solution

Drupal 8's Twig templating engine gives front-end developers and themers a powerful tool for manipulating Drupal markup. But following Drupal's "Working with Twig Templates" guide to a T is likely to create a bloated theme codebase that is difficult to maintain. Instead, with a few small tweaks, we can take advantage of some of Twig's core features to create a more concise and more semantic templating solution.

This method also gives us more refined control over CSS class names and helps alleviate the divitis from which base themes like Classy suffer. It's worth noting that this method will likely result in a large number of template files, so it's helpful to keep them neatly organized within your theme's templates folder. (Drupal searches for template overrides recursively, so don't be stingy with subfolders.)

Let's use the field.html.twig template from Drupal's Classy theme as a starting point.

Classy's field.html.twig

{%
  set classes = [
    'field',
    'field--name-' ~ field_name|clean_class,
    'field--type-' ~ field_type|clean_class,
    'field--label-' ~ label_display,
  ]
%}
{%
  set title_classes = [
    'field__label',
    label_display == 'visually_hidden' ? 'visually-hidden',
  ]
%}

{% if label_hidden %}
  {% if multiple %}
    <div{{ attributes.addClass(classes, 'field__items') }}>
      {% for item in items %}
        <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
      {% endfor %}
    </div>
  {% else %}
    {% for item in items %}
      <div{{ attributes.addClass(classes, 'field__item') }}>{{ item.content }}</div>
    {% endfor %}
  {% endif %}
{% else %}
  <div{{ attributes.addClass(classes) }}>
    <div{{ title_attributes.addClass(title_classes) }}>{{ label }}</div>
    {% if multiple %}
      <div class="field__items">
    {% endif %}
    {% for item in items %}
      <div{{ item.attributes.addClass('field__item') }}>{{ item.content }}</div>
    {% endfor %}
    {% if multiple %}
      </div>
    {% endif %}
  </div>
{% endif %}

This template covers a lot—there is a group of classes added to the field's wrapper element, as well as classes added to the label, and each field value. While the Classy theme is a bit heavy-handed with CSS classes, it does make it easier to style your fields.

But let's say your field is intended to display as a headline. You could duplicate this template and change the element to be an h2, h3, etc., but it's easy to see how doing this across multiple fields would result in a lot of nearly identical code.

Instead, we'll take advantage of Twig's extends functionality to create a default field template that is maintainable, allows for more semantic markup, and requires as little code duplication as possible. This template can be overridden for any field, but will fall back to a reasonable set of defaults.

Using Twig's extends functionality

Twig's extends functionality allows us to use a child template to extend a parent template.

First, we'll handle CSS class inheritance. With a slight modification to the Classy template, we can add the ability to override the classes variable using Twig's ternary shorthand notation. These examples will be written using BEM notation, but can be modified to use any CSS methodology.

field.html.twig

{%
  set classes = classes ?: [
    'field',
    'field--name' ~ field_name|clean_class
  ]
%}

Now, in our child field template, we can override the class(es) that gets added to our field.

field--field-myfield.html.twig

{% extends 'field.html.twig' %}
{% set classes = 'someblock__headline' %}

Next, we will add the ability to override the HTML element in which our field's value is contained.

field.html.twig

{%
  set classes = classes ?: [
    'field',
    'field--name' ~ field_name|clean_class
  ]
%}

{% set element = element ?: 'div' %}

{% if label_hidden %}
  {% if multiple %}
    <div{{ attributes.addClass(classes) }}>
      {% for item in items %}
        <{{ element }}{{ item.attributes.addClass(item_classes) }}>{{ item.content }}</{{ element }}>
      {% endfor %}
    </div>
  {% else %}
    {% for item in items %}
      <{{ element }}{{ attributes.addClass(classes) }}>{{ item.content }}</{{ element }}>
    {% endfor %}
  {% endif %}
{% else %}
  <div{{ attributes.addClass(classes) }}>
    <div{{ title_attributes.addClass(label_class) }}>{{ label }}</div>
    {% for item in items %}
      <{{ element }}{{ item.attributes.addClass(item_classes) }}>{{ item.content }}</{{ element }}>
    {% endfor %}
  </div>
{% endif %}

field--field-myfield.html.twig

{% extends 'field.html.twig' %}
{% set classes = 'someblock__headline' %}
{% set element = 'h2' %}

Note that we can now create a custom field template—complete with custom classes and a configurable HTML element—with just 3 lines of code.

We can extend this pattern further to add options for the element that wraps the field value(s), the class for the label, the element for the label, and the class for each element in a multi-value field. Wrapping the whole thing in a Twig block (named "content" in our example) also allows us to override or add to the markup from a child template.

Finished Product

field.html.twig

{%
  set classes = classes ?: [
    'field',
    'field--name-' ~ field_name|clean_class
  ]
%}

{% set item_classes = item_classes ? item_classes %}

{% set label_class = label_class ?: 'field__label' %}

{% set label_element = label_element ?: 'div' %}

{% set wrapper_element = wrapper_element ?: 'div' %}

{% set element = element ?: 'div' %}

{% block content %}
  {% if label_hidden %}
    {% if multiple %}
      <{{ wrapper_element }}{{ attributes.addClass(classes) }}>
        {% for item in items %}
          <{{ element }}{{ item.attributes.addClass(item_classes) }}>{{ item.content }}</{{ element }}>
        {% endfor %}
      </{{ wrapper_element }}>
    {% else %}
      {% for item in items %}
        <{{ element }}{{ attributes.addClass(classes) }}>{{ item.content }}</{{ element }}>
      {% endfor %}
    {% endif %}
  {% else %}
    <{{ wrapper_element }}{{ attributes.addClass(classes) }}>
      <{{ label_element }}{{ title_attributes.addClass(label_class) }}>{{ label }}</{{ label_element }}>
      {% for item in items %}
        <{{ element }}{{ item.attributes.addClass(item_classes) }}>{{ item.content }}</{{ element }}>
      {% endfor %}
    </{{ wrapper_element }}>
  {% endif %}
{% endblock content %}

Example Usage

Let's say we have a field called "Product Specifications" on a Product content type. This field accepts multiple values, and the design calls for this field to be displayed as a list among some other information about the product. By default, Drupal would output a series of nested DIVs that doesn't indicate any relationship among the content. But with our new field template, we can fix that with a 5 line template.

field--field-product-specifications.html.twig

{% extends 'field.html.twig' %}
{% set wrapper_element = 'ul' %}
{% set element = 'li' %}

{% set classes = ['product-info__spec-list'] %}
{% set item_classes = ['product-info__spec-item'] %}

 

Additional Notes

•          These examples cover only field templates, but the same strategy can be applied to any other type of template in the Drupal ecosystem—blocks, nodes, paragraphs, etc.

•          Drupal, Twig, and BEM all have their own distinct concept of "blocks". So a class of someblock in the examples above doesn't refer to Drupal's concept of a block, nor to a Twig block (see BEM naming and Twig block).