Twig: collecting best practices

Drupal 10
Drupal 9
Basic
Template
Twig
frontend

Template Inheritance Basics

Extends

{% extends %} - the extends tag tells the template engine that this template “extends” another template.

{% extends 'base.html.twig' %}

{% block %} - the block tag is used to tell the template engine that a child template may override those portions of the template. A template that “extends” can’t output anything that isn’t in a block.

{% block content %}
  Overridden content
{% endblock %}

For example:

  1. Template base.html.twig

    {{ title_prefix }}
    {% if title %}
      {{ title }}
    {% endif %}
    {{ title_suffix }}
    
    {% block content %}
      {{ content }}
    {% endblock %}
  2. Template example-template.html.twig which extends base.html.twig

    {% extends 'base.html.twig' %}
    
    {% block content %}
      {% if content %}
        {{ content }}
      {% endif %}
    {% endblock %}

Include 

{% include %} - the include tag tells the template engine to return the rendered content of the included file into the current template.

{% include 'part.html.twig' %}

By default the {% include %} sets the scope of avail­able data to all vari­ables avail­able glob­al­ly in the par­ent template. For example we can set a variable {{ title }} at the top of the main tem­plate and by default, this title vari­able is con­sid­ered in scope for any include tem­plate. 

{% set title = node.title %}

In addi­tion we can also explicitly pass data to the included tem­plate using the with keyword:

{% include 'part.html.twig' with {
    foo1: bar1,
    foo2: bar2,
  }
%}

Also it's possible to disable access to the context by appending the "only" keyword, which allows us restrict the data that is passed into the includ­ed template:

{# no variables will be accessible #}
{% include 'part.html.twig' only %}

This example dis­ables the ​active con­text of the parent template. None of the active con­text data will be avail­able in the includ­ed tem­plate. This includes the title vari­able we set earlier.

{# only the title variable will be accessible #}
{% include 'part.html.twig' with {title: title} only %}

This example restricts data from the ​template active con­text but also includes only the data we explic­it­ly set in using the keyword "with" (the title).

Finally, you can tell the template engine to ignore the statement if the template to be included does not exist by appending the "ignore missing" keyword right after the template name:

{% include 'part.html.twig' ignore missing %}

{% include 'part.html.twig' ignore missing with {'foo': 'bar'} %}

{% include 'part.html.twig' ignore missing only %}

For example:

  1. Template header.html.twig

    <header class="header">
      {% if page.header %}
        {{ page.header }}
      {% endif %}
      
      {% if page.primary_menu %}
        <div id="header__nav--primary" class="header__nav--primary">
          {{ page.primary_menu }}
        </div>
      {% endif %}
      
      {% if page.search %}
        {{ page.search }}
      {% endif %}
    </header>
  2. Template page.html.twig

    {% include 'header.html.twig' with {'page': page} %}
    
    {% if page.breadcrumb %}
      {{ page.breadcrumb }}
    {% endif %}
    
    <main class="main" id="main-content">
      {{ page.content }}
    </main>
    
    {% if page.footer %}
      {{ page.footer }}
    {% endif %}

Embed

{% embed %} - the embed tag combines "include" and "extends" functionalities. It allows to include a template to another template and also to override any block defined inside the included template. 

{% embed 'example-embed.html.twig' %}
  {% block example %}
    Overridden content
  {% endblock %}
{% endembed %}

It also takes the same arguments as the {% include %} tag.

Macro

{% macro %} - the macro tag is useful to reuse template fragments to not repeat yourself.

Good example is a macro for menu links:

{% macro menu_links(items, attributes, menu_level) %}
  {% import _self as menus %}
  {% if items %}
    ...
    {% for item in items %}
      ...
      
        {{ link(item.title, item.url) }}
        {% if item.below %}
          {{ menus.menu_links(item.below, attributes, menu_level + 1) }}
        {% endif %}
      
    {% endfor %}
    
  {% endif %}
{% endmacro %}

We can use a tag {% import %} to import macro into a local variable:

{% import _self as menus %}

And we can use it as

{{ menus.menu_links(item.below, attributes, menu_level + 1) }}

Macro arguments

  1. Arguments of a macro can have a default value, eg

    {% macro menu_links(items, attributes, menu_level = 0) %}
  2. Arguments of a macro are always optional.
  3. If extra positional arguments are passed to a macro, they end up in the special varargs variable as a list of values.

 

Layouts

We can use tags {% extends %}, {% include %}, {% embed %} to build powerful reusable layouts.

Let's see how in works on examples.

At first let's determine namespaces of the helper templates in the theme info file:

# EXAMPLE_THEME.info.yml

....

components:

  namespaces:

    base: components/base

....

Now we will be able to use the extra templates from the directory "components/base" via 

{% TAG '@base/INNER_DIRECTORY/EXAMPLE_TEMPLATE.twig' %}

The directory "components/base" can have hierarchical structure and include as many inner directories as you want.

components/base/card/card.twig

It's common situation most of the listing templates includes items with the similar structure. Starting with building a basic template for the item. Will place it the directory "components/base/card".

# components/base/card/card.twig

{% set attributes = attributes|default(create_attribute()).addClass(['card']) %}
{% set card_title_attributes = card_title_attributes|default(create_attribute()) %}
{% set card_body_attributes = card_body_attributes|default(create_attribute()).addClass(['card-body-wrapper']) %}
{% set card_image_attributes = card_image_attributes|default(create_attribute()).addClass(['card-image-wrapper']) %}

<div {{ attributes }} data-card>
  <div {{ card_body_attributes }}>
    {% if card_title %}
      {{ title_prefix }}
        <h{{ card_title_level }} {{ card_title_attributes }}>
          {{ card_title }}
        </h{{ card_title_level }}>
      {{ title_suffix }}
    {% endif %}

    {% block card_body %}
      {% if card_body %}
        <div class="card-body">
         {{ card_body }}
        </div>
      {% endif %}
    {% endblock %}
  </div>
  
  {% if has_image %}
    <div {{ card_image_attributes }}>
      {% if card_label %}
        <div class="card-label">
          {{ card_label }}
        </div>
      {% endif %}

      {% block card_img %}
        {{ card_img }}
      {% endblock %}
    </div>
  {% endif %}
</div>

The card.twig template includes the following variables: 

HTML attributes:

  • attributes: HTML attributes for the main wrapper element
  • card_title_attributes: HTML attributes for the title element
  • card_body_attributes: HTML attributes for the body wrapper element
  • card_image_attributes: HTML attributes for the image wrapper element

Variables for building the card title:

  • card_title_level: Heading level of the card title
  • card_title: Card title

Variables for building the card content:

  • card_body: Content of the card body
  • card_label: Card label
  • has_image: Determine if the card image exists and should be displayed
  • card_img: Card image

node--VIEW-MODE.html.twig/node--BUNDLE--VIEW-MODE.html.twig

Build the template of the node for required view mode which is used in the listing template based on the card template:

# node--VIEW-MODE.html.twig OR more specified node--BUNDLE--VIEW-MODE.html.twig

{#
/**
 * @file
 * Theme override to display a node.
 *
 * Available variables:
 * ...
 * 
 * @see template_preprocess_node()
 */
#}

{% embed '@base/card/card.twig' with {
  card_body_attributes: create_attribute().addClass(['node-bundle-class'])
  card_title: label,
  card_title_level: 2,
  card_body: content.field_teaser,
  card_label: content.field_lable|default('EXAMPLE LABEL'|t),
  has_image: not node.field_teaser_image.isEmpty(),
} %}
  {% block card_img %}
    {{ content.field_teaser_image }}
  {% endblock %}
{% endembed %}

We use the "embed" tag here which allows us to include the card.twig template and to override the card_img block defined inside it.

components/base/heading/heading.twig

As you can see we use the following structure to build heading section in the card.twig template:

<h{{ card_title_level }} {{ card_title_attributes }}>
  {{ card_title }}
</h{{ card_title_level }}>

We can improve this for more complicated heading section using one more extra template:

# components/base/heading/heading.twig

{% set attributes = attributes|default(create_attribute()) %}

<h{{ heading_level }} {{ attributes }}>
  {% if heading_url %}
    {% set heading_link_attributes = create_attribute().addClass(['card-heading-link']) %}
    <a {{ heading_link_attributes.setAttribute('href', heading_url) }}>{{ heading }}</a>
  {% else %}
    {% if heading_prefix %}
      {% block heading_with_prefix %}
        {{ heading_prefix }} {{ heading }}
      {% endblock %}
    {% else %}
      {% block heading %}
        {{ heading }}
      {% endblock %}
    {% endif %}
  {% endif %}
</h{{ heading_level }}>

The heading.twig template includes the following variables: 

HTML attributes:

  • attributes: HTML attributes for the main wrapper element

Variables for building the heading structure:

  • heading_level: Heading level of the element

Variables for building the heading content:

  • heading_url: The href attribute of the link to wrap the whole heading section to
  • heading: Main content of the heading section
  • heading_prefix: Content which can be displayed before the main content of the heading section

Include this heading.twig template into the card.twig template:

# components/base/card/card.twig

{% set attributes = attributes|default(create_attribute()).addClass(['card']) %}
{% set card_title_attributes = card_title_attributes|default(create_attribute()) %}
{% set card_body_attributes = card_body_attributes|default(create_attribute()).addClass(['card-body-wrapper']) %}
{% set card_image_attributes = card_image_attributes|default(create_attribute()).addClass(['card-image-wrapper']) %}

<div {{ attributes }} data-card>
  <div {{ card_body_attributes }}>
    {% if card_title %}
      {{ title_prefix }}

        <!-- Include the heading.twig template -->
        {% include "@base/heading/heading.twig" with {
          heading_level: 2,
          heading: card_title,
        } only %}

      {{ title_suffix }}
    {% endif %}

    {% block card_body %}
      {% if card_body %}
        <div class="card-body">
         {{ card_body }}
        </div>
      {% endif %}
    {% endblock %}
  </div>
  
  {% if has_image %}
    <div {{ card_image_attributes }}>
      {% if card_label %}
        <div class="card-label">
          {{ card_label }}
        </div>
      {% endif %}

      {% block card_img %}
        {{ card_img }}
      {% endblock %}
    </div>
  {% endif %}
</div>

 

So, as you can see, you can build multi-level inheritance model of the templates using these instruments. The multi-level model is a best-practice method so that the base template for a bundle can be easily overridden to properly extend your application's base layout.

References