7 tip

7 Tricks for Creating a Drupal Theme that You May Have Never Heard Of

Theme creation in Drupal is not as easy as it may seem. Therefore, there is space here to make your work easier. Often, however, we take shortcuts for this purpose, but these do not always bring the desired results or contradict Drupal's philosophy and generally accepted best practices.

Probably many of you know that as in any other CMS or framework we work on, in Drupal, we can achieve the same goals by choosing different paths to reach them. In the course of my experience, I have faced common mistakes made by Drupal beginners repeatedly, and I have managed to track many codes created by novice and experienced Drupal developers. I have seen how much different the approaches they use. As part of this article, I would like to introduce 7 tricks I have selected (or just shortcuts) that make it easier to work on creating a theme, which many programmers I have never heard of.

Due to the fact that with each release of the Drupal subversion there are various significant changes, I would like to emphasize the examples discussed here that are compatible with the core 8.9.x version. All presented tricks, although they may be sometimes controversial in use, they are fully consistent with the art and can be used for projects.

1. Use of the #context key in the rendering array

One of the common problems when creating templates is the ability to pass data between them. This usually requires programmers to perform complex operations, e.g. when processing templates. The challenge becomes even more difficult when some hooks have already been cached and data upload was still needed. There may be many ideas for solving the problem, but with the Drupal 8.8 version, the problem is solved by the #context key.

Suppose we have implemented hook_preprocess_node, in which we add some additional element using a dedicated template, but we would like to still have access to the node object in it. This is where the #context key comes in handy.

function my_module_preprocess_node(array &$variables) {
  $node = $variables['node'];

  $variables['my_custom_element'] = [
    '#theme' => 'my_custom_theme',
    '#title' => t('My custom theme title'),
    '#context' => [
      'entity' => $node,
    ],
  ];
}

This makes it possible to pass any data from one rendering table to another. You can fit anything in it, e.g. an instance of an object that we will need in further processing:

function my_module_preprocess_my_custom_theme(array &$variables) {
  $node = $variables['#context']['entity'];
  // Do whatever you needed with the node entity in your custom theme...
}

It's worth mentioning that this variable is also available in hook_theme_suggestion_alter, which can further facilitate making suggestions for additional context-dependent template names.

2. Cache support in twig templates

More and more demanding graphic designs of websites, as well as page optimization (in terms of the amount of HTML code returned), forces us to use some shortcut solutions, e.g. extracting the field value directly in the entity template (node type).

Solutions such as

{{ content.field_my_reference_field.0 }}

displaying only single field instead of the content variable can have disastrous consequences in the form of cache problems. In the rendering arrays, apart from the final HTML code, there are also keys responsible for storing such cache metadata: tag, context and max-age. If you want to be sure that the metadata will be collected correctly by the renderer service, just add one magic line in the template:

{% set rendered = content|render %}

In the example of a node template, it will be the content variable, but it can be any "main" variable of your template. Passing such a main rendering array through the render filters allows you to correctly collect all cache metadata, without having to display the entire content variable. At the same time, the solution does not have any negative impact on the performance of code execution, as long as we use cache mechanisms.

3. Theme suggestion without using hook_theme_suggestion_

Yes, it sounds like something completely crazy. However, this solution is available "out of the box". It is enough to add the suggestion to the values hidden in #theme or theme_hook_original in any rendering array (e.g. in hook_preprocess_HOOK) keeping the template naming convention.

As a result, the variable value set in this way

function my_module_preprocess_paragraph(array &$variables) {
  $paragraph = $variables['paragraph'];

  if ($paragraph->bundle() === 'my_awesome_bundle') {
    $variables['theme_hook_original'] = 'paragraph__my_custom_theme_suggestion';
  }
}

records a suggestion for a given base template (paragraph), allowing you to create a template file named: paragraph-my-custom-theme-suggestion.html.twig Nevertheless, I would limit this solution to exceptional cases. For general use, I would recommend a hook dedicated for this purpose.

4. Safe filter replacement | raw

This filter is generally considered to be potentially dangerous (increases the possibility of successful XSS attacks) and is used in single cases. But it should be avoided as much as possible. I suggest looking for a better way, especially since one of them is extremely easy to use, as you will see in an example.

Let's assume that we have a field that is to finally display the image, but for some reason we would like to omit all the HTML code generated along the way.

The easiest way to do this is:

{{ content.field_my_image_field|render|striptags('<img>')|trim }}

Experienced Drupal developers know that such a solution, instead of displaying an image on the page, will display the whole as plain text encoded into HTML entities. Yes, you can add a raw filter, but you can do it differently using Drupal's Render API with the #markup key:

{{ {'#markup': content.field_my_image_field|render|striptags('<img>')|trim} }}

The method is perhaps a bit less readable than with the filter, but it is safe. HTML code will still be filtered here and only allowed safe tags will be passed through.

5. Referencing a template file from a different theme/module

If you haven't used Twig mechanisms such as include, embed or extend in your template files, I suggest to take the DRY rule into account in every aspect of programming (or web development in general). Themes are the places that suffer most from duplicate code, both in template files and in style sheets.

Many times I have encountered the entire template file was copied, e.g. from an inherited theme or a module, only to add some surrounding HTML code or modify a single template block. The same effect can be achieved by referring to a parent file using the aliases @module_name @theme_name. Suppose we have a template file (card) in our sample module named custom_card

<div class="card">
  {% block card_header %}
    <div class="card__header">{{ header }}</div>
  {% endblock %}
  
  {% block card_content %}
    <div class="card__content">{{ content }}</div>  
  {% endblock %}
  
  {% block card__footer %}
    <div class="card__footer">{{ footer }}</div>
  {% endblock %}
</div>

And now in our theme, we would need to add some additional elements in the header block, e.g. an icon. The most common thing is to copy the entire template code, but this can be done more simply and without repeating the code:

{% extends '@custom_card/card.html.twig' %}

{% block header %}
    <div class="card__header">
      <div class="icon">{{ icon }}</div>
      {{ header }}
    </div>
{% endblock %}

6. View reloading using InvokeCommand

As you know, the Views module allows to create views that use AJAX technology. This is done to dynamically switch between the different pages of the view, but also to display the results of the response to value changes from the provided form. But what to do when the content of the view needs to be refreshed due to performing some action outside it, e.g. after sending a request to the controller from another block? Or to submit a form on the same page? Well, there are two very convenient functionalities available in the Drupal core.

The first one is the default RefreshView event registered on the view, which causes the view to be refreshed using AJAX (I recommend that you enter it in the browser console on your example view and observe the effect).

The second mechanism is InvokeCommand, which allows in the AJAX response to return the jQuery method call, along with the given parameters on the DOM element. Combined, this gives you this example code:

class MyCustomController extends ControllerBase {

  /**
   * Perform the update and refresh the view.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   Ajax response.
   */
  public function doUpdateAndRefresh(): AjaxResponse {
    // Perform some sort of logic needed for your controller.
    $this->doUpdateOperations();

    // Refresh the view.
    $response = new AjaxResponse();
    $response->addCommand(new InvokeCommand('#my-view-block', 'trigger', ['RefreshView']));

    return $response;
  }

}

In my example, there were forms for user approval inside the generated view. In response to the entry of such a form, I needed to display an updated list. As you can see, basically one extra line did all the magic without any extra effort.

7. Use of Drupal.debounce

For the very end I want to describe something which is not so much a trick as good practice. The Underscore.js library is bundled with Drupal core, but many developers do not use it at all. It includes a number of functionalities. One of the most common cases where the capabilities of this library are not used is the so-called deboucing, i.e. preventing multiple code execution in response to a given event - most often it is a change in the width of the browser window.

I have come across code many times that resembled:

function onResize(callback) {
  var timer;
  return function (event) {
    if (timer) {
      clearTimeout(timer);
    }
    timer = setTimeout(callback,100, event);
  };
}

$(window).resize(onResize(function(e) {
  // Resize implementation goes here.
}));

Of course, this is an exemplary and the simplest implementation, and such code will execute correctly. The question is: why reinvent the wheel? Answer: to improve Drupal compatibility in the core of Drupal, and also to increase the convenience of work for developers - there is a debouncer port from the underscore library.

Just add a dependency to your library

- core/drupal.debounce

and add the following implementation in the JS code:

$(window).resize(Drupal.debounce(function () {
  // Resize implementation goes here.
}, 100));

Isn't it easier? And most importantly, we use something that has already been well written once.

Summary

Although I have come across a lot of interesting approaches to working with themes in our Drupal agency, it must be emphasized that many things come with experience. Especially novice developers should follow Drupal's philosophy from the very beginning. They should understand why they are doing something in a certain way. This allows us to see Drupal's capabilities more easily and learn to use them to increase the comfort and efficiency of work, without losing the cleanliness of the code.

Best