Reusable style guide components using field formatters and twig embed
Share:
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.
by
Jack Taranto
/ 16 November 2017
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 Styleguide section 6.1 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.
Tagged
Posted by
Jack Taranto
Front end developer
Dated 16 November 2017
Add new comment