Upgrade Your Drupal Skills

We trained 1,000+ Drupal Developers over the last decade.

See Advanced Courses NAH, I know Enough

Reusable style guide components using field formatters and twig embed

Parent Feed: 

At PNX, style guide driven development is our bag. It’s what we love: building a living document that provides awesome reference for all our front end components. And Drupal 8, with its use of Twig, complements this methodology perfectly. The ability to create a single component, and then embed that component and its markup throughout a Drupal site in a variety of different ways without having to use any tricks or hacks is a thing of beauty.

Create a component

For this example we are going to use the much loved collapsible/accordion element. It’s a good example of a rich component because it uses CSS, JS, and Twig to provide an element that’s going to be used everywhere throughout a website.

To surmise the component it’s made up of the following files:

collapsible.scss
collapsible.widget.js
collapsible.drupal.js
collapsible.twig
collapsible.svg

The .scss file will end up compiling to a .css file, but we will be using SASS here because it’s fun. The widget.js file is a jQuery UI Widget Factory plugin that gives us some niceties - like state. The drupal.js file is a wrapper that adds our accordion widget as a drupal.behavior. The svg file provides some pretty graphics, and finally the twig file is where the magic starts.

Let’s take a look at the twig file:

{{ attach_library('pnx_project_theme/collapsible') }}
<section class="js-collapsible collapsible {{ modifier_class }}">
  <h4 class="collapsible__title">
    {% block title %}
      Collapsible
    {% endblock %}
  </h4>
  <div class="collapsible__content">
    {% block content %}
      <p>Curabitur blandit tempus porttitor. Cum sociis natoque penatibus et
        magnis dis parturient montes, nascetur ridiculus mus. Morbi leo risus,
        porta ac consectetur ac, vestibulum at eros. Praesent commodo cursus
        magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac
        cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo
        sit amet risus.</p>
    {% endblock %}
  </div>
</section>

This is a standard-ish BEM based component. It uses a js-* class to attach the widget functionality. We also have a {{ modifier_class }} variable, that can be used by kss-node to alter the default appearance of the collapsible (more on this later). There are two elements in this component title and content. They are expressed inside a twig block. What this means is we can take this twig file and embed it elsewhere. Because the component is structured this way, when it’s rendered in its default state by KSS we will have some default content, and the ability to show it's different appearances/styles using modifier_class.

Our twig file also uses the custom Drupal attach_library function which will bring in our components CSS and JS from the following theme.libraries.yml entry:

collapsible:
  css:
    component:
      src/components/collapsible/collapsible.css: {}
  js:
    src/components/collapsible/collapsible.widget.js : {}
    src/components/collapsible/collapsible.drupal.js : {}
  dependencies:
    - core/jquery
    - core/drupal
    - core/jquery.once
    - core/jquery.ui
    - core/jquery.ui.widget

This is a pretty meaty component so it’s got some hefty javascript requirements. Not a problem in the end as it’s all going to get minified and aggregated by Drupal Cores library system.

And there we have it - a rich javascript component. It’s the building block for all the cool stuff we are about to do.

Use it in a field template override

As it stands we can throw this component as-is into KSS which is nice (although we must add our css and js to KSS manually, attach_library() won’t help us there sadly - yet), but we want drupal to take advantage of our twig file. This is where twigs embed comes in. Embed in twig is a mixture of the often used include, and the occasionally used extend. It’s a super powerful piece of kit that lets us do all the things.

Well these things anyway: include our twig templates contents, add variables to it, and add HTML do it.

Because this is an accordion, it’s quite likely we’ll want some field data inside it. The simplest way to get this happening is with a clunky old field template override. As an example I’ll use field--body.html.twig:

{% for item in items %}
  {% embed '@pnx_project_theme/components/collapsible/collapsible.twig' %}
    {% block title %}
      {{ label }}
    {% endblock %}
    {% block content %}
      {{ item.content }}
    {% endblock %}
  {% endembed %}
{% endfor %}

Here you can see the crux of what we are trying to achieve. The collapsible markup is specified in one place only, and other templates can include that base markup and then insert the content they need to use in the twig blocks. The beauty of this is any time this field is rendered on the page, all the markup, css and js will be included with it, and it all lives in our components directory. No longer are meaty pieces of markup left inside Drupal template directories - our template overrides are now embedding much richer components.

There is a trick above though, and it’s the glue that brings this together. See how we have a namespace in the embed path - all drupal themes/modules get a twig namespace automatically which is just @your_module_name or @your_theme_name - however it points to the theme or modules templates directory only. Because we are doing style guide driven development and we have given so much thought to creating a rich self-contained component our twig template lives in our components directory instead, so we need to use a custom twig namespace to point there. To do that, we use John Albins Component Libraries module. It lets us add a few lines to our theme.info.yml file so our themes namespace can see our component templates:

component-libraries:
  pnx_project_theme:
    paths:
      - src
      - templates

Now anything in /src or /templates inside our theme can be included with our namespace from any twig template in Drupal.

Use it in a field formatter

Now let’s get real because field template overrides are not the right way to do things. We were talking about making things DRY weren’t we?

Enter field formatters. At the simple end of this spectrum our formatter needs an accompanying hook_theme entry so the formatter can render to a twig template. We will need a module to give the field formatter somewhere to live.

Setup your module file structure as so:

src/Plugin/Field/FieldFormatter/CollapsibleFormatter.php
templates/collapsible-formatter.html.twig
pnx_project_module.module
pnx_project_module.info.yml

Your formatter lives inside the src directory and looks like this:

<?php

namespace Drupal\pnx_project_module\Plugin\Field\FieldFormatter;

use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * A field formatter for trimming and wrapping text.
 *
 * @FieldFormatter(
 *   id = "collapsible_formatter",
 *   label = @Translation("Collapsible"),
 *   field_types = {
 *     "text_long",
 *     "text_with_summary",
 *   }
 * )
 */
class CollapsibleFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
    $elements = [];

    foreach ($items as $delta => $item) {
      $elements[$delta] = [
        '#theme' => 'collapsible_formatter',
        '#title' => $items->getFieldDefinition()->getLabel(),
        '#content' => $item->value,
        '#style' => NULL,
      ];
    }

    return $elements;
  }

}

And the hook_theme function lives inside the .module file:

<?php

/**
 * @file
 * Main module functions.
 */

/**
 * Implements hook_theme().
 */
function pnx_project_module_theme($existing, $type, $theme, $path) {
  return [
    'collapsible_formatter' => [
      'variables' => [
        'title' => NULL,
        'content' => NULL,
        'style' => NULL,
      ],
    ],
  ];
}

Drupal magic is going to look for templates/collapsible-formatter.html.twig in our module directory automatically now. Our hook_theme template is going to end up looking pretty similar to our field template:

{% embed '@pnx_project_theme/components/collapsible/collapsible.twig' with { modifier_class: style } %}
  {% block title %}
    {{ title }}
  {% endblock %}
  {% block content %}
    {{ content }}
  {% endblock %}
{% endembed %}

Now jump into the field display config of a text_long field, and you’ll be able to select the collapsible and it’s going to render our component markup combined with the field data perfectly, whilst attaching necessary CSS/JS.

Add settings to the field formatter

Let's take it a bit further. We are missing some configurability here. Our component has a modifier_class with a mini style (a cut down smaller version of the full accordion). You'll notice in the twig example above, we are using the with notation which works the same way for embed as it does for include to allow us to send an array of variables through to the parent template. In addition our hook_theme function has a style variable it can send through from the field formatter. Using field formatter settings we can make our field formatter far more useful to the site builders that are going to use it. Let's look at the full field formatter class after we add settings:

class CollapsibleFormatter extends FormatterBase {

  /**
   * {@inheritdoc}
   */
  public function viewElements(FieldItemListInterface $items, $langcode = NULL) {
    $elements = [];

    foreach ($items as $delta => $item) {
      $elements[$delta] = [
        '#theme' => 'collapsible_formatter',
        '#title' => !empty($this->getSetting('label')) ? $this->getSetting('label') : $items->getFieldDefinition()->getLabel(),
        '#content' => $item->value,
        '#style' => $this->getSetting('style'),
      ];
    }

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsSummary() {
    $summary = [];
    if ($label = $this->getSetting('label')) {
      $summary[] = 'Label: ' . $label;
    }
    else {
      $summary[] = 'Label: Using field label';
    }
    if (empty($this->getSetting('style'))) {
      $summary[] = 'Style: Normal';
    }
    elseif ($this->getSetting('style') === 'collapsible--mini') {
      $summary[] = 'Style: Mini';
    }
    return $summary;
  }

  /**
   * {@inheritdoc}
   */
  public function settingsForm(array $form, FormStateInterface $form_state) {
    $form['label'] = [
      '#title' => $this->t('Label'),
      '#type' => 'textfield',
      '#default_value' => $this->getSetting('label'),
      '#description' => t('Customise the label text, or use the field label if left empty.'),
    ];
    $form['style'] = [
      '#title' => t('Style'),
      '#type' => 'select',
      '#options' => [
        '' => t('Normal'),
        'collapsible--mini' => t('Mini'),
      ],
      '#description' => t('See <a href="https://www.previousnext.com.au/styleguide/section-6.html#kssref-6-1" target="_blank">Styleguide section 6.1</a> for a preview of styles.'),
      '#default_value' => $this->getSetting('style'),
    ];
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public static function defaultSettings() {
    return [
      'label' => '',
      'style' => '',
    ];
  }

}

There's a few niceties there: It allows us to set a custom label (for the whole field), it automatically assigns the correct modifier_class, it links to the correct section in the style guide in the settings field description, and it adds a settings summary so site builders can see the current settings at a glance. These are all patterns you should repeat.

Let's sum up

We've created a rich interactive BEM component with its own template. The component has multiple styles and displays an interactive demo of itself using kss-node. We've combined its assets into a Drupal library and made the template - which lives inside the style guides component src folder - accessible to all of Drupal via the Component Libraries module. We've built a field formatter that allows us to configure the components appearance/style. Without having to replicate any HTML anywhere.

The component directory itself within the style guide will always be the canonical source for every version of the component that is rendered around our site.

Posted by Jack Taranto
Front end developer

Dated

Add new comment

Author: 
Original Post: 

About Drupal Sun

Drupal Sun is an Evolving Web project. It allows you to:

  • Do full-text search on all the articles in Drupal Planet (thanks to Apache Solr)
  • Facet based on tags, author, or feed
  • Flip through articles quickly (with j/k or arrow keys) to find what you're interested in
  • View the entire article text inline, or in the context of the site where it was created

See the blog post at Evolving Web

Evolving Web