2017 Aug 21

Here are some interesting tidbits pulled from our daily workflow using Twig. This was going to be a general twig topics article, but since we use it extensively in Drupal, this also includes Drupal specific tips as well that only apply in that context.

1. Loop variables

It can be helpful to know which loop iteration you're on, to figure out if you need to continue outputting delimiters or dividers, and if you need to satisfy some other logic such as "only do this on the first iteration" as an example.

A common use of this is when we output tags for nodes. For example, within the node type override file for fields, we might have something like this:

{% if field_name == 'field_category' %}
  <span{{ attributes }}>
    {% for item in items %}
      {{ item.content }}{% if not loop.last %},{% endif %}
    {% endfor %}
  </span>

Further documentation on loop variables  

2. Containment Operator

In this example, we use the containment operator to test for the existence of a link. In this case, we turned it into an anchor if the string "http" is contained within the field text:

  {% set website_text = items|first.content['#context'].value %}
  <span{{ attributes }}>
    {% if 'http' in website_text %}
      <a href="{{ items|first.content }}">{{ items|first.content }}</a>
    {% else %}
      {{ items|first.content }}
    {% endif %}
  </span>

Further documentation on the containment operator  

3. String Interpolation

Folks may relate this to PHP's ability to output variables within double quotes as part of a string. While not used often, it can come in handy. An example might be on an account centre to welcome a user. This example assumes a variable called "given_name" is set and available withing the current template scope.

{{ "Welcome #{given_name}" }}    

Further documentation on String Interpolation

4. Translating strings for Drupal

Typically there's only two flavors of this we use day-to-day. The first being a singular replacement, and the second involving surrounding text with the string as a parameter.

First the simple version:

{{ "My string to translate"|t }}

Next, the contextual version with text around the variables:

{% trans %} Submitted by {{ author.username }} on {{ node.created }} {% endtrans %}

Further documentation on translating strings in Drupal 8

5. Batching output

This one is useful for making groupings of output. One way we use it is to make lists (or menus) split out into equal columns. The example here is simpler which shows how you might get this result:

<table>
    <tr>
        <td>a</td>
        <td>b</td>
        <td>c</td>
    </tr>
    <tr>
        <td>d</td>
        <td>e</td>
        <td>f</td>
    </tr>
    <tr>
        <td>g</td>
        <td>No item</td>
        <td>No item</td>
    </tr>
</table>

 

{% set items = ['a', 'b', 'c', 'd', 'e', 'f', 'g'] %}

<table>
{% for row in items|batch(3, 'No item') %}
    <tr>
        {% for column in row %}
            <td>{{ column }}</td>
        {% endfor %}
    </tr>
{% endfor %}
</table>

Further documentation on twig batching

6. Macros

Macros are import to understand as Drupal uses them in its menu templates. They are comparable to functions in regular programming languages. Drupal 8 menu templates use them recursively to build out the menu structure, not unlike how it was done in Drupal 7 with straight PHP. We've actually setup our front end static inventory structure to use twig, and ported this idea of macros to it for stubbing out menus. Here is what that looks like to create a static menu macro that could easily be ported right into a Drupal 8 menu template. This example was used to create a mega menu, hence the extra markup involved:

{% import _self as menus %}

{# https://mijingo.com/blog/key-value-arrays-in-twig #}
{% set items = {
    about:    { title: "Google", url: "http://google.com",
              "below": {
                services: { title: "Industrial Agency 1", url: "http://industrialagency.ca" },
                services2: { title: "Industrial Agency2 ", url: "http://industrialagency.ca" },
                services3: { title: "Industrial Agency3", url: "http://industrialagency.ca" }
              }
    },
    services: { title: "Industrial Agency", url: "http://industrialagency.ca" },
    services2: { title: "Industrial Agency2 ", url: "http://industrialagency.ca" },
  }
%}

{% macro menu_links(items, attributes, menu_level) %}
  {% import _self as menus %}
  {% if items %}
    {% if menu_level == 0 %}
      <ul class="nav__menu">
    {% else %}
      <ul>
    {% endif %}
    {% for item in items %}

      {% if item.in_active_trail %}
      <li{{ item.attributes.addClass('active-trail') }}>
      {% else %}
      <li{{ item.attributes }}>
      {% endif %}

        {% if menu_level == 0 and item.below %}
          <div class="drawer-wrapper">
            <a href="{{item.url}}" class="drawer-toggle" aria-expanded="false">{{item.title}} <i class="fa fa-angle-down" aria-hidden="true"></i></a>
            <div class="nav__submenu drawer" aria-expanded="false">
              <div class="centered-content">
                <div class="grid">
                  <div class="grid__col--md-3">
                    <ul>
                      {% for item in item.below %}
                        <li><a href="{{item.url}}"><span>{{item.title}}</span></a></li>
                      {% endfor %}
                    </ul>
                  </div>
                </div>
              </div>
            </div>
          </div>
        {% else %}
          <a href="{{item.url}}"><span>{{item.title}}</span></a>
          {% if item.below %}
            {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
          {% endif %}
        {% endif %}

      </li>
    {% endfor %}
    </ul>
  {% endif %}
{% endmacro %}

{#
  We call a macro which calls itself to render the full tree.
  @see http://twig.sensiolabs.org/doc/tags/macro.html
#}
<nav class="nav nav--mega">
{{ menus.menu_links(items, attributes, 0, directory) }}
</nav>

Further documentation on twig macros

7. Includes

Typically these are useful for creating component files for common elements like social sharing icons, or headers and footers.

{% include directory ~ '/templates/includes/footer.html.twig' %}

Further documentation on Twig includes

8. Retrieving URL from URI with file_url

Drupal uses URI's to store references to assets. To convert them to URL's, a helper function has been added that maps back to file_create_url function. Here's an example taken from a field level template for a document upload field:

{% if field_name == 'field_document' %}

  <span{{ attributes }}>
    {% for item in items %}
      <a target="_blank" href="{{ file_url(item.content['#file'].uri.value) }}">
        {% if item.content['#description'] %}
          {{item.content['#description']}}
        {% else %}
          {{item.content['#file'].filename.value}}
        {% endif %}
      </a>
      {% if not loop.last %},{% endif %}
    {% endfor %}
  </span>

Change record outlining the addition of this into Drupal 8

9. Creating Wordpress style "Page Templates" for Drupal

This one is a bit of an extension on the twig discussion, but does have to do with being able to create custom templates. Drupal never really had this as an option. In other words, being able to create custom templates and being able to choose them from a drop down in the backend.

To accomplish this, we typically create a custom term vocabulary called "Page Templates" and assign terms that will correspond to template name suggestions. Then, in node and page level preprocess functions within the Drupal 8 theme, we have this code which provides normalized suggestions based on the term names.

Here is the page level preprocess function, we also do this for the node one too:

/**
* Implements HOOK_theme_suggestions_HOOK_alter() for page templates.
* Each new suggestion here makes a new page preprocess function available to use
*/
function MYTHEME_theme_suggestions_page_alter(array &$suggestions, array $variables) {
  //https://sqndr.github.io/d8-theming-guide/theme-hooks-and-theme-hook-suggestions/theme-hook-suggestions.html
  $node = \Drupal::routeMatch()->getParameter('node');
  if (is_numeric($node)) $node = node_load($node);
  if ($node) {
    // content type suggestions, similar to core node suggestions, but for page templates
    $suggestions[] = 'page__'.$node->getType();
    // custom templates built from a term vocabulary
    if ($node->hasField('field_page_templates') && $node->field_page_templates->getValue()) {
      $tid = $node->field_page_templates->getValue()[0]['target_id'];
      if ($tid) {
        $template = \Drupal\taxonomy\Entity\Term::load($tid)->getName();
        // use underscores for spaces so we can create preprocess functions
        // for all the custom templates. Drupal seems to change these into dashes
        // where the actual template name is concerned though.
        $suggestions[] = 'page__'.preg_replace("/[^a-zA-Z0-9s]/", "_", strtolower($template));
      }
    }
  }
}

Shows the current template being used
For this page, we'll be using the custom template for "Policy Blog Landing". This is a term field on the basic page content type.
twig template list for page level templates.
Here we see the new page level suggestion being used at the top beside the "X"

10. Quickly see all available variables in the current template scope:

To dump all available variables and their contents in a template, add the following to your template (after enabling debugging):

{{ dump() }}

To dump only the available variable keys use:

{{ dump(_context|keys) }}

The above is handy to use so you don't lock your browser trying to output the large amount of data that can come with the variables available to the template. While it's not strictly twig related, but it is to debugging in general, I'll throw in a link to Kint usage in Drupal 8

Further Resources: