Twig Concepts in Drupal 8 Themes - Part II

In the previous post I covered how to use the Component Libraries module and Twig to create simple reusable components, using an SVG sprite icon template as an example.

Part 2: Applying Twig Concepts to Write Better Code

When learning Drupal theming, overriding templates is one of the key topics of interest. It’s a simple thing. Copy the source template into your theme, modify it and clear the cache. Easy! However, doing just that over and over again, can lead to a mess of unmaintainable code.

A good example of this is the page.html.twig template. This template is the main layout driver of a theme. Changes to the page template can have far reaching consequences, though. When I inherit a site to maintain, one of the first things I look for is how many page templates the theme contains. If there are more than a handful, or even more than one that seem unnecessary, I start to cringe. For example, if I’m looking at a theme with multiple page templates, which a client requests a change to, no matter how big or small that change is, I now need to get familiar with all templates and try to find out the intentions behind their creation, and how I can implement that change successfully, or risk a botched deployment. This usually involves comparing the files using a diff app, looking back and commit history, and also searching through CSS. It can turn a seemingly simple task into one that takes hours.

Why does this sort of thing happen? At a basic level, this is what we were taught to do. There’s a ton of ground to cover with markup coming from many different sources, and blindly overriding is often faster and gets the job done. Beyond theme hook suggestions, which provide some clues about intent, I find that there’s generally been an inability to express clear and concise intentions in code alone, in Drupal themes. What I mean by that specifically: I want to immediately understand why a variant of page.html.twig exists without having to spend much time thinking about it, and comparing by trudging through a sea of code.

Twig is a lifesaver in this regard.

In this post, I’ll provide details about how and when to use Twig’s include, block, extends and embed tags, and in a way that helps makes your intentions for overriding templates clearer to anyone else that might work with them the future.

Include

The Twig include tag is similar to PHP include function, in that the purposes is to include another file. The include tag also provides access to that template’s variables. It’s pretty straightforward. In the Part I’s icon component implementation example, I used it twice. First, to inline the contents of our icons.svg file in html.html.twig:

{% include active_theme_path() ~ '/img/icons.svg' %}

Second, to demonstrate using the icon component template, using the with keyword to pass a variable:

{% include '@components/icon.twig' with { icon: 'twitter' } %}

Summary

Allows for including one file into another, with ability to manipulate variables and context via special keywords.

  • No special structure is required for the contents of the include file.
  • You may include as many files as you want in a given template.
  • Variables can be accessed using the with {} keyword, and also directly in the file being included.
  • Context can be disabled or limited with the only keyword.
  • Missing templates/files can be made fail silently with the ignore missing keyword.

Block

Twig’s documentation describes them like this: “Blocks are used for inheritance and act as placeholders and replacements at the same time. They are documented in detail in the documentation for the extends tag.” That’s a bit confusing, and they are documented in much more detail, but I’ll try to explain.

I find that the concept of Twig Blocks is similar to the concept of theme regions in Drupal. Blocks are essentially sections of code that as the template designer, you decide should be changeable from some other template that implements them. Let’s look at a real world example of how we might take advantage of that, in the context of page.html.twig.

First, here’s a very simplified version of what you might find in page.html.twig. It’s got a wrapper, basic header, logo, title, main area with a column and sidebar, and a footer:

{% set classes = ['page'] %}
<div{{ attributes.addClass(classes) }} id="page">
  <header role="banner">
    <a href="{{ path('<front>') }}" title="{{ 'Home'|t }}" rel="home" class="logo">
      {% include '@components/icon.twig' with { icon: ‘logo’ } %}
    </a>
    {{ page.header }}
  </header>
  <main role="main">
    <a id="main-content" tabindex="-1"></a>
    <div class="content">
      {{ page.breadcrumb }}
      {{ page.highlighted }}
      {{ page.help }}
      {{ page.content }}
    </div>
    {% if page.sidebar_first %}
      <aside class="sidebar" role="complementary"> {{ page.sidebar_first }} </aside>
    {% endif %}
  </main>
  {% if page.footer %}
    <footer role="contentinfo"> {{ page.footer }} </footer>
  {% endif %}
</div>

Note: I’m not using the Site branding block, which normally comes through the header region.

As is, I cannot extend this template, so if I need another page template for whatever reason, the answer is to copy and paste, which I’d prefer not to do. Instead, let’s wrap what we might want to override elsewhere in Twig blocks and make this template into something that can be changed or built upon from its variants:

{% set classes = ['page'] %}
<div{{ attributes.addClass(classes, add_classes) }} id="page">
  <header role="banner">
    {% block logo %}
      <a href="{{ path('<front>') }}" title="{{ 'Home'|t }}" rel="home" class="logo">
        {% include '@components/icon.twig' with { icon: 'twitter' } %}
      </a>
    {% endblock %}
    {% block header %} {{ page.header }} {% endblock %}
  </header>
  <main role="main">
    <a id="main-content" tabindex="-1"></a>
    {% block main %}
    <div class="content">
      {{ page.breadcrumb }}
      {{ page.highlighted }}
      {{ page.help }}
      {% block content %} {{ page.content }} {% endblock %}
    </div>
    {% if page.sidebar_first %}
      <aside class="sidebar" role="complementary">
        {% block sidebar %} {{ page.sidebar_first }} {% endblock %}
      </aside>
    {% endif %}
    {% endblock %}
  </main>
  {% if page.footer %}
    <footer role="contentinfo">
      {% block footer %} {{ page.footer }} {% endblock %}
    </footer>
  {% endif %}
</div>

In the code above I defined the following 6 areas as Twig blocks: header, logo, main, content, sidebar, and footer. By creating these blocks, I’ve made this template extendable in areas I’ve decided are “variable” based on the design requirements of a given site. Twig blocks have nothing to do with Drupal, and Drupal has no means of interacting with them.

I’ve purposely defined the blocks inside the HTML wrappers of these areas because I’d like extending templates to easily change the contents, not the wrappers themselves. I could have taken this a step further, creating blocks for both the outer and inner areas, of these same sections for times when changing the outer wrapper is needed.

The addition of these blocks by don’t actually “do” anything in the context of this template. The markup contained within block tags, prints just as if those tags didn’t exist. If you are reading and thinking WTF, stick with me. The power of this functionality comes into play when using Twig Blocks in combination with Extends or Embed...

Extends

Twig’s extends tag exists to facilitate extending one template to another, with the use of Twig blocks and a parent/child relationship. The parent template contains the structure in the form of blocks (like our example code above) and the child template uses the template via the extends tag, from a “child” template implementation. When combined with theme hook suggestions, this opens up some really cool possibilities, as we can see with some simple examples below:

Example: Remove the sidebar from the homepage.

Using theme hook suggestions, create a template called page--front.html.twig, and use the extends tag to reference the page.html.twig template. Then, underneath, create a block with the same name of the Twig block (sidebar), created in page.html.twig, and don’t put anything inside it. This has the effect of overriding and emptying the area.

{#
/**
 * @file
 * Overrides the homepage, removing the sidebar.
 */
#}
{% extends 'page.html.twig' %}
{% block sidebar %}{% endblock %}

Example: Wrap a DIV around the content and sidebar.

Using theme hook suggestions, create a template called page--front.html.twig, and use the extends tag to reference the page.html.twig template. Then, underneath, create a block with the same name of the Twig block (main), created in page.html.twig. Inside this block, create the DIV, and use the parent() function to print the contents of the Twig block as defined in the current template.

{#
/**
 * @file
 * Overrides the homepage, wrapping the main area in a div.
 */
#}
{% extends 'page.html.twig' %}
{% block main %}
  <div class="another-div">
    {{ parent() }}
  </div>
{% endblock %}

Summary
Extends facilitates a parent/child relationship with templates, using Twig blocks to manage the structure in the parent template. The child template may then choose to modify, override or inherit from the parent as desired by implementing block tags with the same name.

  • Can be used once per rendering.
  • Child templates have access to variables in both the outer and block level scopes.
  • Child templates can override or “empty” blocks, as shown in the first example.
  • Child templates can partially override a block’s content, or move it to a different location, without duplicating code, using the parent() function, as shown in the second example.
  • Code is not allowed outside of block tags in a child template. You can add new blocks to get around this, however that will not help in all cases, and if you run into this, chances are you probably want to use embed (see below). For example, if you add a new Twig block (one that doesn’t exist in the template you’re extending) with HTML it will work just fine, but if you try to use attach_library() in that same block, the library will not load.
  • You cannot pass variables into the extends tag using with {}, like you can with include or embed.

Embed

The Twig Embed tag combines the functionality of include and extend, giving the best of both worlds. The syntax is a bit more verbose, but it’s a lot more flexible. As you can probably gather from the descriptions of Include and Extends, there are some pretty clear differences of between two:

  • Include is very basic, limited functionality. The most complex thing you can do with it is reference a file to include, and pass it keywords and/or variables.
  • Extend is more complex, since it works with blocks. While its’ purpose is to allow you extend one template from another, there are limitations that make it difficult in some common use cases, such as only being able to use it once per template, and limiting content to inside blocks.

However, there are use cases where you want to be able to combine the functionality of these two tags, and that’s what the Embed tag is for. It gives the best of both worlds. It removes the limitations set in extends, allowing you to use it multiple times in a file, provides more flexibility around structure (or lack thereof), and it allows you to control the variables using with {}, only, and/or ignore missing keywords.

All of the examples I’ve provided in this post can be done with the Embed tag:

{# Include example using embed. #}
{% embed '@components/icon.twig' with { icon: 'twitter' } %}{% endembed %}

{# page--front.html.twig that overrides sidebar using embed. #}
{% embed 'page.html.twig' %}
  {% block sidebar %}{% endblock %}
{% endembed %}

{# page--front.html.twig that adds wrapper div ands uses parent() with embed. #}
{% embed 'page.html.twig' %}
{% block main %}
  <div class="another-div">
    {{ parent() }}
  </div>
{% endembed %}

Summary
Embed combines the utility of include and extends.

  • The structure of the embedded file is flexible; blocks are optional.
  • You can use embed and include tags as often as you need in a given file.
  • You can nest embed tags, e.g. one embed inside another.
  • Variables can be accessed in the embed tag using with {} keyword.
  • Variables can be limited in the embed tag using the only or ignore missing keyword.
  • Twig blocks can be overridden inside the Embed tag.
  • The parent() function can be used via blocks inside the embed tag to partially or fully override a block’s content.
  • Embedded templates have access to variables in both the outer and block level scopes.

These patterns are useful in Drupal contexts, beyond small components and the page template. They're just as useful for everything in between, from content types that have a relatively similar design, down to field and form level. As you start to get deeper into using Twig, you'll find that you can combine these techniques together to create more complex components that fit your use case.

I hope this post helps you to start thinking about ways you might write your Twig templates in a DRY, clear and concise manner. Beyond that, I hope you become inspired to take some control over architecture back. You no longer have to work within the constraints of a box that Drupal has provided for architecture, so get excited about that, and challenge yourself to create something great.

Theo Ballew, and Gabe Guevara contributed to this post.

Topics