Preamble
I've been working with (and loving) Pattern Lab for the past year and a half, and trying to find time to share some of the most valuable lessons I've learned. This tutorial is the first in what I hope will become a series of articles that demonstrate how I'm solving common requests and requirements for our projects. The underlying concept here does not require Pattern Lab, though I find it a particularly flexible and (mostly) intuitive way to create highly predictable, modular and reusable components.
Background
The paragraphs module has become a central ingredient for many component-based sites in recent years. However, our content strategy also often requires components that display dynamic content (think "Read Next", or "Also of Interest"). In this tutorial, I'll demonstrate how we've been solving this problem, by building paragraph bundles that serve as configuration entities that we can then use as arguments that we pass to a view via the Twig Tweak module. You can see a working version of the dynamic content component we'll be building in the "Up Next" card grid at the bottom of this tutorial.
Creating the View
We want our content editors to be able to add dynamic content to a page that includes a heading, the tag that should be used as a filter, and the number of items they want to display. To start with, let's create a view that is configured as a block of teasers with two contextual filters, the tag (or tags), and a filter that excludes a particular node id. The latter will prevent the current node from displaying within the block. I'm not going to offer a detailed explanation of building views in Drupal, since this is likely quite familiar to most readers.
Building the Paragraph Bundle
Once the view has been configured, it's time to create the paragraph bundle. I'll call it "Tagged content", and add the "configuration" fields we will need in order to render the dynamic content described above.
Once the bundle exists and the fields are present, we can ignore the "Manage display" interface, since we're going to circumvent the render pipeline via Twig Tweak. However, if we add a tagged content bundle to a page (in this case, I already have a content type that allows me to add paragraphs bundles to the page), we should see the values that we set being rendered to the screen via the paragraphs module base template.
The next step, therefore, is creating a paragraph item template that is responsible for rendering the view rather than the actual content of the item, using the "configuration settings" from the paragraph item that we just created.
Twig Field Value and Twig Tweak FTW
Our "configuration" entity is now ready for rendering the view. To do that, we'll leverage the Twig Field Value module to get the values from the paragraph bundle and pass them to the view via Twig Tweak. Here's how that looks in the paragraph item template file (paragraph--tagged-content.html.twig):
{% set tids = content.field_referenced_tags['#items'] is not empty
? content.field_referenced_tags|field_raw('target_id')|safe_join(',')
: 'all' %}
{% set limit = content.field_number_of_items|field_raw('value') %}
{% set heading = content.field_heading|field_raw('value') %}
{# Leverage twig_tweak to create the render array #}
{% set view = drupal_view('tagged_content', 'block_1', tids, nid, limit, heading) %}
{{ view }}
After rebuilding the cache and refreshing the page, we'll now see the content teasers being rendered, though our "Number of items" setting isn't yet working. Also, the value of nid
is undefined, which is what we need in order to exclude the current node from the tagged content list. In order to accomplish those two things, as well as prepare for future component hook implementations, we'll create a small custom module called "component helper". The relevant hooks are:
/**
* Implements hook_preprocess_entity().
*/
function component_helper_preprocess_paragraph__tagged_content(&$variables) {
if ($current_node = \Drupal::request()->attributes->get('node')) {
$variables['nid'] = $current_node->id();
}
}
and
/**
* Implements hook_views_query_substitutions().
* Sets the number of items based on the paragraph bundle setting
* Using this hook instead of alter so that queries can be cached.
*/
function component_helper_views_query_substitutions(ViewExecutable $view) {
if ($view->id() == 'tagged_content') {
$limit = (isset($view->args[2])) ? $view->args[2] : 0;
if ($limit > 0) {
$view->query->setLimit($limit);
}
}
}
Rendering the View Through Pattern Lab
Now we're ready to map our variables in Drupal and send them to be rendered in Pattern Lab. If you're not familiar with it, I suggest you start by learning more about Emulsify, which is Four Kitchens' Pattern Lab-based Drupal 8 theme. Their team is not only super-helpful, they're also very active on the DrupalTwig #pattern-lab channel. In this case, we're going to render the teasers from our view as card molecules that are part of a card grid organism. In order to that, we can simply pass the view rows
to the the organism, with a newly created view template (views-view--tagged-content.html.twig):
{# Note that we can simply pass along the arguments we sent via twig_tweak #}
{% set heading = view.args.3 %}
{% include '@organisms/card-grid/card-grid.twig' with {
grid_content: rows,
grid_blockname: 'card',
grid_label: heading
} %}
Since the view is set to render teasers, the final step is to create a Drupal theme template for node teasers that will be responsible for mapping the field values to the variables that the card template in Pattern Lab expects.
Generally speaking, for Pattern Lab projects I subscribe to the principle that the role of our Drupal theme templates is to be data mappers, whose responsibility it is to take Drupal field values and map them to Pattern Lab Twig variables for rendering. Therefore, we never output HTML in the theme template files. This helps us keep a clean separation of concerns between Drupal's theme and Pattern Lab, and gives us more predictable markup (note more, since this only applies to templates that we're creating and adding to the theme; otherwise, the Drupal render pipeline is in effect). Here is the teaser template we use to map the values and send them for rendering in Pattern Lab (node--article--teaser.html.twig):
{% set img_src = (img) ? img.uri|image_style('teaser') : null %}
{% include "@molecules/card/01-card.twig" with {
"card_modifiers": 'grid-item',
"card_img_src": img_src,
"card_title": label,
"card_link_url": url,
} %}
If you're wondering about the img
object above, that's related to another custom module that I wrote several years ago to make working with images from media more user friendly. It's definitely out of date, so if you're interested in better approaches to responsive images in Drupal and Pattern Lab, have a look at what Mark Conroy has to say on the topic. Now, if we clear the cache and refresh the page, we should see our teasers rendering as cards (see "Up Next" below for a working version).
Congrats! At this point, you've reached the end of this tutorial. Before signing off, I'll just mention other useful "configuration" settings we've used, such as "any" vs. "all" filtering when using multiple tags, styled "variations" that we can leverage as BEM modifiers, and checkboxes that allow a content creator to specify which content types should be included. The degree of flexibility required will depend on the content strategy for the project, but the underlying methodology works similarly in each case. Also, stay tuned, as in the coming weeks I'll show you how we've chosen to extend this implementation in a way that is both predictable and reusable (blocks, anyone?).