Feeds

Upgrade Your Drupal Skills

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

See Advanced Courses NAH, I know Enough
Jun 08 2021
Jun 08

There's a neat little Drupal module called JSON Field and recently, I had a chance to play around with it. Out of the box, JSON field is a just a plain field where JSON data can be input and output on a web page. On its own, the module does not do much beyond just printing the raw data formatted as JSON. However, I got to thinking it would be ideal to nicely format the data with HTML. In this article, I will show you how I accomplished this with both a preprocess function and some custom code in Twig.

Getting started

First, you'll want a Drupal 8 or 9 instance running. In the root of your project, run:

composer require drupal/json_field

Note, if you get an error, you may need to append a version number, for example:

composer require drupal/json_field:1.0-rc4

Next, enable the module and create a new field on an entity, for example on a page content type. When I created my field, I chose the option, JSON stored as raw JSON in database

Next, input some JSON data, for sample data, I like to use Mockaroo. (At a high level, I could envision using the Drupal Feeds module to import JSON data in bulk and mapping it to a JSON field but I have not tested this.)

An example of the Mockaroo interface showing mock data being generatedAn example of the Mockaroo interface showing mock data being generated

Create a preprocess function

We are rendering this data in a node so I have a basic node preprocess function setup below with a sub-theme of Olivero called Oliver. Within this, we will leverage Xdebug to examine the data up close. We write this code in our theme's .theme file.

Define and check for an instance of a node

The first thing we want to do is, since we are working within a node context, we will set some node definitions and check to ensure that we are on a node page.

At the top of our file, we will add

use Drupal\node\NodeInterface;

Then, within the function, we will define the node.

  // Define the node.
  $node = \Drupal::routeMatch()->getParameter('node');

Now we check for an instance of a node:

  // If instance of a node.
  if ($node instanceof NodeInterface) {

Field PHP magic

I named my JSON field field_json_raw and we first want to check to see if the field exists and that it is not empty. For this, I like to use a PHP magic method. A magic method is a short cut of sorts to dig into the field data. In Drupal terms, this looks like:

 if ($node->hasField('field_json_raw') &&
   !$node->get('field_json_raw')->isEmpty()) {
	// Custom code here...
  }

The magic methods above are hasField and get.

Start up Xdebug

Next up, we will use Xdebug to examine the data output to see how we might prepare variables for our Twig template. Within the node preprocess function, I set an Xdebug breakpoint and start listening. Once Xdebug is running we can evaluate the field expression using the magic method again but this time adding the value on. For example, $node->get('field_json_raw')->value. That ends up with the plain value of the field:

[
  {
    "id": 1,
    "country": "Cuba",
    "animal_name": "Cape Barren goose",
    "description": "Curabitur convallis.",
    "animal_scientific": "Cereopsis novaehollandiae"
  },
  {
    "id": 2,
    "country": "Vietnam",
    "animal_name": "Horned puffin",
    "description": "Pellentesque ultrices mattis odio.",
    "animal_scientific": "Fratercula corniculata"
  }
]
etc...
Xdebug showing the plain value of the JSON outputXdebug showing the plain value of the JSON output

Convert the value into an array

What we need to do now is convert that to a usable PHP array. We use the json_decode function:

 // Set a variable for the plain field value.
 $json_raw = $node->get('field_json_raw')->value;
 // Convert the data into an array using json decode.
 $json_array = json_decode($json_raw);

That ends up looking like this:

Xdebug showing the converted JSON arrayXdebug showing the converted JSON array

Create a template variable

Now we have a nicely formatted array to loop through once inside a twig template. The final piece is to check for valid JSON and set the template variable. Note, JSON field already check for valid json but it's probably good practice to do this anyway.

 // Check for valid JSON.
 if ($json_array !== NULL) {
  // Create a variable for our template.
  $vars['json_data'] = $json_array;
 }

Finished preprocess function

Putting it all together, our entire preprocess function looks like this:

getParameter('node');
  // If instance of a node.
  if ($node instanceof NodeInterface) {
    // Check for the field and that it is not empty.
    if ($node->hasField('field_json_raw') &&
      !$node->get('field_json_raw')->isEmpty()) {
      // Set a variable for the field value.
      $json_raw = $node->get('field_json_raw')->value;
      // Convert the data into an array.
      $json_array = json_decode($json_raw);
      // Check for valid JSON.
      if ($json_array !== NULL) {
        // Create a variable for our template.
        $vars['json_data'] = $json_array;
      }
    }
  }
}

Render the JSON variable in Twig

Now we'll go into our Twig template and render the variable with a loop. At its very most basic, it will look something like this:


    {% if content.field_json_raw | render %}
      {% for item in json_data %}
      {{ item.animal_name }}
      {{ item.animal_scientific }}
      {{ item.country }}
      {% endfor %}
    {% endif %}

Of course we want to add HTML to this to make it look nicely styled and here is where you can do most anything you want. I opted for a data table:


    {% if content.field_json_raw | render %}
      

{{ 'Animals from around the world'|t }}

{% for item in json_data %} {% endfor %} {{ 'Animal Name'|t }} {{ 'Scientific Name'|t }} {{ 'Country'|t }} {{ item.animal_name }} {{ item.animal_scientific }} {{ item.country }}
{% endif %}

The code above ends up looking like this:

The finished styled output of the JSON data Twig loopThe finished styled output of the JSON data Twig loop

Summary

And there you have it, nicely styled JSON data rendered in a Twig template. Using the JSON Field module might not be a common everyday item but it definitely fulfills a specific use case as outlined here.

Resources

Jun 02 2021
Jun 02

For my local Drupal development environment over the past few years, I’ve been quite happy using Docksal for my projects. Recently, I onboarded to a new project that required me to use Lando, another popular local development server. In addition, I use PHPStorm for coding, an integrated development environment or IDE. When I am coding, I consider Xdebug to be of immense value for debugging and defining variables.

Xdebug is an extension for PHP, and provides a range of features to improve the PHP development experience. [It's] a way to step through your code in your IDE or editor while the script is executing.

Getting started: the basic setup

In this article I will share with you my basic setup to get up and running with Xdebug 3. The core of this tutorial requires Lando and PHPStorm to be running on your machine. You can head over to GitHub to grab the latest stable release of Lando. Installing Lando will also install Docker desktop, a containerized server environment. You'll also need PHPStorm to be installed as well. If you have any issues with getting Lando running, you can check their extensive documentation. You can spin up a Drupal 9 site using Lando but as of yet, there is no option that will set it up as a true composer based workflow. Therefore, you can use Composer itself composer create-project drupal/recommended-project to create a new Drupal 9 project and then initialize it with Lando. In this case, you'd need Composer 2 to be setup globally on your local machine but once Lando is up and running, you can then switch to using lando composer... Thereafter, be sure to install Drupal as well.

Configure the Lando recipe

Lando has the notion of "recipes" for specific server setups and you'll want to set up a basic Drupal 9 site running on Lando. The TL;DR for the one I am using is below. This code would go in your .lando.yml file in the root of your project. Noting that any time your change your recipe, you will need to run lando rebuild -y.

name: d9sandbox
recipe: drupal9
config:
  php: "7.4"
  composer_version: "2.0.7"
  via: apache:2.4
  webroot: web
  database: mysql:5.7
  drush: true
  xdebug: true
  config:
    php: lando/config/php.ini

services:
  node:
    type: node:12.16
  appserver:
    xdebug: true
    config:
      php: lando/config/php.ini
    type: php:7.4
    overrides:
      environment:
        PHP_IDE_CONFIG: "serverName=appserver"

# Add additional tooling
tooling:
  node:
    service: node
  npm:
    service: node

Of note in the above code:

  1. Xdebug set to true
  2. PHP set to 7.4
  3. MySQL set to 5.7
  4. Composer set to 2.x
  5. Pointer to a custom php.ini file where we will need additional configuration for Xdebug.

Configure php.ini

Now, we need to create our php.inifile within lando/config. Create these directories if they do not exist already. You'll want to set these variables for Xdebug:

[PHP]
xdebug.max_nesting_level = 256
xdebug.show_exception_trace = 0
xdebug.collect_params = 0
xdebug.mode = debug
xdebug.client_host = ${LANDO_HOST_IP}
xdebug.client_port = 9003
xdebug.start_with_request = yes
xdebug.log = /tmp/xdebug.log

I also like to add memory_limit = -1 to this file as well to avoid any out of memory issues. Also note that port 9003 is set for Xdebug, that is because Xdebug 3 requires this. If you've used an older version of Xdebug in the past, the port was typically 9000.

Rebuild Lando

Now that we have Lando all set, go ahead and run lando rebuild -y so that these updates take effect. Once that is done, you'll see something like this in terminal:

NAME            d9xdebug
LOCATION        /Users/me/Projects/d9xdebug
SERVICES        appserver, database, node
APPSERVER URLS  https://localhost:55165
                http://localhost:55166
                http://d9sandbox.lndo.site/
                https://d9sandbox.lndo.site/

Once that is done, go to your site and make sure it loads. In my case the url is http://d9sandbox.lndo.site/.

Configure PHPStorm

Now we are ready to configure PHPStorm. Here, we will connect Docker to the IDE. The following below is a series of screen captures that illustrate how to set this up.

Choose a language level and a CLI interpreter

The first thing to do is to go to preferences and search for PHP. Here you will want to set PHP 7.4 as the language level. Next click on the three vertical dots to choose a CLI interpreter.

Choosing a language level and a CLI interpreter in PHPStormChoosing a language level and a CLI interpreter in PHPStorm

Configure the CLI interpreter

Here you will choose "From Docker..."

Configuring the CLI interpreter in PHPStormConfiguring the CLI interpreter in PHPStorm

Choose the CLI image name

Here you will choose "Docker" and then the image name, in our case devwithlando/php:7.4-apache-2. Now click apply and ok to save all these settings.

Choosing the CLI image nameChoosing the CLI image name

Trigger Xdebug

Now for the final step, we are ready to trigger Xdebug with the steps below.

  1. Input a variable to set a breakpoint for. In the example, below, I've set a fake variable, $a =1;within function bartik_preprocess_node(&$variables){...}
  2. Run lando drush cr
  3. Click in the left margin parallel to the variable so that you see a red dot.
  4. Click on the Phone icon at the top of the IDE so that it turns green. This is the "listener" where we set PHPStorm to listen for incoming connections so as to trigger Xdebug.
  5. Now click accept after choosing index.php. This is a standard convention.
  6. If all goes well, you will now see Xdebug get triggered. Click “Accept.”
Triggering Xdebug in the IDETriggering Xdebug in the IDE

View Xdebug output

Once the above is set, you should now see the actual output from Xdebug as shown below. Thereafter, you can inspect arrays to your heart’s content!

View the Xdebug outputView the Xdebug output

Tips and Tricks

  • I have found that occasionally, if other Lando projects are running, Xdebug will sometimes not be triggered so it might be best to only run one project at a time. To do that, run these commands in the root of your project.
lando poweroff
lando start
  • If for some reason the listener does not trigger Xdebug, stop it and clear Drupal cache. Then start listening again.

Summary

Hopefully this article has helped to get up and running with Lando and Xdebug 3 with PHPStorm. I cannot stress enough how useful Xdebug is when working on a Drupal website. I’ve seen folks struggle with using Kint, print_r(), and var_dump(). While these are somewhat useful, to me nothing compares to the speed, preciseness, and usefulness of Xdebug.

Resources

Sep 27 2020
Sep 27

I've been building Drupal 8 sites with Layout Builder over the past year. One feature that I think is missing is the ability to group specific fields inside a wrapper for enhanced theming; a layout within a layout if you will.

There are a few issues open in core for this feature but these seem to be just in the planning stage right now. There are some workarounds to achieve this feature.

  • Create a view with an argument that just renders the fields in context to the entity item they are a part of.
  • Use an entity view via the contrib. module, Chaos Tool Suite, AKA Ctools.

It is the second solution above that I will write about here as it is my favorite one and one which a developer friend of mine told me about. The idea behind Entity View is that you add some fields to a custom view mode and then render these as one chunk inside your default Layout Builder display. Think of it as a layout within a layout. Entity View is not a "View" from the Views module, rather it is a bundled group of fields referencing a specific view mode.

Enable / install modules

For this method to work, you will need a Drupal site on either 8.8.x + or Drupal 9.0.x. as well as Ctools. Since Layout Builder is in core, but is disabled by default, it will need to be enabled as well as core's Layout Discovery module. To get Ctools, you can download it via composer, composer require drupal/ctools

Once Ctools has been downloaded, enable these modules either via the admin UI or drush.

Modules to be enabled via the Drupal 8/9 admin UIModules to be enabled via the Drupal 8/9 admin UI

Getting Started

Now that we have the required modules enabled, we will need to enable Layout builder for an entity type. In my case, I have a content type called "Person" which features people involved with an organization. I navigate to /admin/structure/types/manage/person/display and check the box under Layout Options, "Use Layout Builder." Note that this is done under the default view mode.

Enable Layout Builder in the Drupal admin UIEnable Layout Builder in the Drupal admin UI

Entity view use case

Here is an example use case for an entity view:

  1. We have a 2 column layout section in Layout Builder with several "person" fields such as email, phone, fax, company address, social media links, etc.
  2. In one of the columns, we want to have a "profile card" which will feature a person's image along with a few contact fields all themed as one item.
  3. We have other fields before and after the profile card.

With these specs above, we need a way of grouping our profile card fields all together as one unit, thus we will use an entity view. We are doing this as normally, Layout builder creates a "block" for each field so it would be tricky to theme these if we do not have a "wrapper" around our designated fields that will be part of the profile card.

Visualize it

Before building something, I often find it helpful to visualize it with a mockup. The image below illustrates what we are after.

The profile card within a Layout builder column that will be built using entity view.The profile card within a Layout builder column that will be built using entity view.

A recipe

Here is a list of "ingredients" and some instructions that are needed to build this:

Create a custom view mode for content called "contact card."

  1. Go to/admin/structure/display-modes/view
  2. Add new Content view mode at/admin/structure/display-modes/view/add/node
  3. Set the name as "Contact card" and save

Enable and set the new view mode

  1. Go to /admin/structure/types/manage/person/display
  2. Navigate to the display for the new view mode at /admin/structure/types/manage/person/display/contact_card
  3. Set the fields you'd like to display for the contact card.
Enabling the contact card view mode on the profile content type.Enabling the contact card view mode on the profile content type.

Configure Layout Builder for entity view

  1. Go back to /admin/structure/types/manage/person/display and click on Manage Layout which will bring you into the main layout Builder user interface.
  2. Here you will remove the fields that you just set above for the contact card view mode by hovering over each field and using the "Remove block" contextual link.
  3. Now for the fun part, we will setup the entity view.
    _ Click in a section and "Add block."
    _ You will see an option in the sidebar for "Entity view (Content)
    _ Click and add that and set to the desired view mode, in our case "Contact Card."
    _ Save the layout and preview!
Adding an entity view to the main default Layout Builder layoutAdding an entity view to the main default Layout Builder layoutEnabling the contact card view mode on the profile content type.Enabling the contact card view mode on the profile content type.

One more thing

Now that we have our entity view up working, we may want to enhance this by adding a custom class to the new block for theming. This comes in handy if you are using BEM (Block / Element / Modifier) CSS styling as you can add the block class to the parent block element. To do so, you can either use Layout Builder Styles or Layout Builder Component Attributes.

Summary

Entity view solves a specific problem for Layout Builder if you need an inner wrapper within a section. Note that I did not find very much documentation about this method so I thought it would be a good idea to document herein.

Resources

May 25 2020
May 25

Over the past month, I have been working on a large scale Drupal 8 build that is leveraging the Layout Builder module for much of the site's displays. If you are not familiar with Layout Builder, it's a fairly new initiative that hit Drupal 8 core as an experimental module in 8.5 and then gained full status in 8.7.

So far, I am really enjoying using Layout Builder, my point of view being very much form a theming perspective. In my option, Layout Builder is almost like a hybrid between Panels and Paragraphs. It's ideal for content-centric sites where you'd like to hand over more control of content display to editors while keeping to a structured modular content model. This paradigm should sound vaguely familiar for those folks who have been building with Paragraphs.

"Drupal 8's Layout Builder allows content editors and site builders to easily and quickly create visual layouts for displaying content. Users can customize how content is arranged on a single page, or across types of content, or even create custom landing pages with an easy to use drag-and-drop interface."

Overview

Here are some highlights that I think are worth noting.

  • Works on all different kinds of entities, media, nodes, blocks, Paragraphs, and more
  • Drag and drop goodness in the Admin UI, similar to Panels
  • Pulls in all kinds of entities from fields, blocks, custom modules, views, user information, system, and more
  • Limit entities that can be selected on a per content type basis (or allow all!)
  • UI option to Override a layout on a per node basis per content type
  • Large ecosystem via contrib. modules
  • Extensible via custom code, for example defining a new layout in a theme or module
  • One-off Layout Builder custom blocks created in the UI.
  • Excels at the creation of structured modular content for content editors.
  • Layouts export to config (drush cex) with the exception of those that have been overridden on an entity.

While the Paragraphs module has been all the rage for the past several years, Layout Builder has gained more prominence recently, though I suspect Paragraphs will be around for some time to come. I have not tried it, but I am guessing you could even bring in Paragraphs to Layout builder so you would have a joining of the two. On the site I am working on now, we are using standard fields and blocks as our main Layout Builder content.

The Drupal 8 Layout Builder drag and drop admin UI showing how blocks and sections are laid outThe Drupal 8 Layout Builder drag and drop admin UI showing how blocks and sections are laid out

Kicking the tires

If you have not used Layout Builder yet for a project and have been wanting to jump in, I hope this article will serve as a good overview. So far, I have learned a lot by just playing around with it. The basic setup happens on a an entity's manage display admin UI where you can check a box to have Layout Builder take over and manage the display of a given view mode.

Speaking of view modes

I've found view modes to come in quite handy with Layout Builder. There are a few different ways we are using view modes here. One is to set other view modes that Layout builder blocks can reference. For example, if you are in a Default view mode for your main layout, you could also setup another view mode, say "Related" for your main blocks to use for their displays on Default.

Another method is to setup an entirely different view mode as a Layout Builder layout and then give content editors an option to switch view modes per node. I figured out how to use hook_entity_view_mode_alter to switch our Layout Builder display based on a field value, I'll probably write up a dedicated post for that. I did see mention of a contrib. module called Layout Builder Library which allows content editors to switch layouts from a predefined list so that might be a good way to go as well. I have not tried that module so I can not speak to how it works.

Layout Builder Styles

One contrib. module I have found to be invaluable is Layout Builder Styles. This allows for setting pre-defined HTML classes for use in the Layout Builder UI for both sections and blocks. You can choose to allow for multiple classes per element or just one. Since I write my Sass using BEM, I tend to add some of the section classes as BEM modifiers so as to fit these classes into my existing Sass workflow seamlessly.

Another advantage of using Layout Builder Styles is that once you assign a class to an element, it then creates custom theme hooks based on your class. Imagine if you will, a number of blocks with the same class that you want themed in a specific manner, you could use the same template for all of these based on a class that has been set. Note, there is an open issue with an RTBC patch that fixes cases where these theme hooks do not work. I tested the patch here installing via composer and it does fix the issue.

Layout Builder Modal

This is another very useful contrib. module in the Layout Builder ecosystem. This module takes some of the configuration out of the very narrow native off-canvas dialog and turns it into a configurable modal which to me if much more editor friendly. It also gives you the option to switch to the admin theme while in the modal which I like very much.

Mind the admin UI

One thing that I noticed is, since the Layout Builder UI uses your front-end theme, there may be some cases where your theme CSS might do some unexpected things on the admin side. For these instances, I created a _layout-builder.scss file where I use a class that only renders within the admin UI, for example, layout-builder__section and make a few updates straighten things out.

Overrides

If you have an entity type that allows per entity override with Layout Builder, keep in mind that once you override that entity, the layout will no longer be kept track on in config. That's expected behavior but does allow for more flexibility for content editors. For those cases, you might limit what entities can be selected in the main entity settings. For example, we see these three radio button options available on the entity config display settings for "Content Fields."

  1. Allow all existing & new Content fields blocks.
  2. Allow specific Content fields blocks (whitelist):
  3. Restrict specific Content fields blocks (blacklist):

Summary

One thing that caught me out in the Layout Builder blocks UI was that I was expecting to be able to put an HTML wrapper around certain blocks within a section but that functionality does not exist yet. There are a few open issues for that feature request. Think Field groups within the blocks layout UI.

Overall, I think Layout Builder in Drupal 8 core is an amazing and very useful tool that will be around for some time to come including the upcoming release of Drupal 9, June 2020.

Key Resources, Issues, and APIs

May 23 2020
May 23

In a new Drupal 8 build that I am working on, there is a use case for some menu items to have an abbreviation underneath. I wanted to create a content editor friendly way of achieving this in the Drupal menu admin UI. Since I have already implemented the Drupal 8 Link Attributes widget module, I got to thinking that perhaps I could extend the module to handle these abbreviations.

Extending the Link Attributes widget

I was happy to discover that Link Attributes can be extended to any kind of additional HTML attribute you might need for your menu. I came up with the idea that if I could add a new field for the abbreviation, I could turn it into an HTML data attribute for a menu item and then output it with a little jQuery.

There are two pieces to extending the widget within a custom module, my_module.

  1. Leverage function my_module_entity_base_field_info_alter to add the new attribute desired.
  2. Create a custommy_module.link_attributes.ymlfile and add your attribute in there.

Function

Our function looks like this:

function my_module_entity_base_field_info_alter(&$fields, \Drupal\Core\Entity\EntityTypeInterface $entity_type) {
  if ($entity_type->id() === 'menu_link_content') {
    $fields['link']->setDisplayOptions('form', [
      'type' => 'link_attributes',
      'settings' => [
        'enabled_attributes' => [
          'id' => FALSE,
          'name' => FALSE,
          'target' => TRUE,
          'rel' => FALSE,
          'class' => TRUE,
          'accesskey' => FALSE,
          'data-menu-abbreviation' => TRUE,
        ],
      ],
    ]);
  }
}

YAML

The key from the above code is 'data-menu-abbreviation' => TRUE, to let Drupal know, we want a field with that name. Once that is done, we can write a little code in our my_module.link_attributes.yml file as to how this will look in the menu UI. Note that we match the attribute name from entity_base_field_info_alter.

data-menu-abbreviation:
  title: Menu Abbreviation

Once you clear cache, the menu UI will now show the new field we created.

The Drupal 8 menu admin UI showing the new field for an abbreviation attributeThe Drupal 8 menu admin UI showing the new field for an abbreviation attribute

Because we use data- here, this attribute will render as an HTML5 data attribute which is ideal to leverage with jQuery. Inspecting on the front end, we see the menu item render as:

Rendered HTML


This is a great, we now have our abbreviation within the menu link. The next step is to read the data in jQuery and render that as a span tag within the menu item.

Pull the data in via jQuery

Now in our custom theme JS file, we can loop through the menu items and output each data attribute like so:

// Get the menu data attribute and render as a span tag.
$(context)
  .find(".custom-menu__item--level-1 > a")
  .once("menu-abbreviation")
  .each(function () {
    // If there is a data attribute, "menu-abbreviation".
    if ($(this).data("menu-abbreviation")) {
      // Set a variable.
      var menu_abbreviation = $(this).data("menu-abbreviation");
      // Output the tag from the data.
      $(this).append("" + menu_abbreviation + "");
    }
  });

After this code, we now see the output look like this:


This works great and solves a very specific use case but as you can imagine, this type of customization could come in handy for a lot of menu needs.

Gotchas

There are a few gotchas here, it sounds like hook_entity_base_field_info_alter may be changed or deprecated in Drupal 9 so be aware of that when upgrading. (see the issue listed below in resources). You also might need to use hook_module_implements_alter to adjust the weight of your custom code to be sure it fires after link_attributes if your module is named with a letter starting before the letter "L".

Resources

Feb 01 2020
Feb 01

Working with custom datetime elements in Drupal can sometimes be a bit tricky when you need a very specific date format to render. Even trickier can be date ranges that span multiple days. Add on top of that, date ranges that span over 2 different months and you might have your work cut out for you.

I am currently working on theming a Drupal 8 site that will feature events and event dates in a prominent way. In this article, I will create a recipe for how I am coding this within a Twig template. The context here is a landing page view where we show a listing of events with a basic date - month, day, and year.

The format I want to achieve in the end is:

  • Multi-Day Event - e.g. Jan 29 - 31 2020
  • Multi-Day Event (spanning over 2 different months) e.g. Jan 29 - Feb 03 2020
  • One Day Event - e.g. Jan 29 2020

We'll be using Twig's date formatter so if you need a worldwide date format, i.e. non-American, it will be a case of just re-arranging when we code up our custom formats. For example:

format_date('custom', 'M d Y')

would be:

format_date('custom', 'd M Y')

Getting started

For this project, I am using Drupal 8, a few core modules, and one contrib module. This article assumes the following knowledge:

  • Installing Drupal modules with Composer
  • Setting up a local development environment (I use Docksal)
  • Using Xdebug in your IDE (I use PHPStorm)
  • Knowledge of writing Twig code
  • Knowledge of basic Drupal 8 site building

Core

  • Drupal core 8.7.11
  • Datetime
  • Datetime Range

Contrib

  • Twig Xdebug (Optional)

As you can see, we have Datetime as well as Datetime Range which allows us to have a field formatter for event dates that span over multiple days. Once you're up and running, get twig_xdebug module with composer:

composer require drupal/twig_xdebug

Now, enable the modules:

drush en datetime datetime_range twig_xdebug -y

Create an event content type with date field

The next step is to create an event content type, Events, with a date field using Date range as the type. We don't need to be concerned with the display settings here as we will be doing our formatting in our Twig template.

The image above shows the datetime range formatter in the Drupal 8 UI.The image above shows the datetime range formatter in the Drupal 8 UI.

Create a custom view mode / events view

The next step is to create a custom view mode for our events landing page. I'll call it "Event List." That custom view modes are now a part of D8 core is a nice plus here. Now create a basic view for the event content type and set the format to Content > Event List, whereby we use the new view mode just created. For the view, we can choose either a page or block display depending on the use case.

Create a custom Twig template

The next step is to create a few event nodes so we can load up our new events list landing page and start to debug. The first thing is to make sure you have Twig template debugging enabled in your local development environment. Since we want a custom template for theming, I look at the twig template suggestion debugging output and see this:


As you can see from the debug output above, we have a nice variety of template suggestions to choose from. I'll use node--events--event-list.html.twig which is specific to the content type, view, and view mode we are using.

Note that a huge advantage of theming views in this manner using view modes is that you can theme as if it were a regular node template. In that regard, I grab Classy's basic node.html.twig file and place it in my custom theme's templates folder using the custom name as above. Now run drush cr and re-check the Twig template output and ensure that the new template has taken effect.

Debug with Twig Xdebug

For debugging with Twig, I like to use Twig Xdebug as mentioned above, which piggybacks on top of Xdebug so you can debug right in your Twig template with one line of code, {{ breakpoint() }}.

When I start to debug, the output provides me with a wealth of information that we can leverage to create some custom date variables right within our twig template. (Note that you could also use standard Xdebug within a .theme or .module file via a preprocess function.)

$context["content"]["field_event_date"][0]["start_date"]["#attributes"]["datetime"] = 2020-02-19T12:00:00Z
The image above shows Twig Xdebug printing out variables in PHPStorm.The image above shows Twig Xdebug printing out variables in PHPStorm.

From the above debugging output, we have the date and time in what's known as UTC Time ISO-8601 Format. We'll be converting that later to a Unix timestamp with Twig so that in turn, we can create custom, nicely formatted dates.

Setting variables in Twig

Now that we have debugging info, we can set some variables right in Twig like this:


 {# Define event start / end dates. #}
    {% set event_start_date = content.field_event_date.0.start_date["#attributes"]["datetime"] %}
    {% set event_end_date = content.field_event_date.0.end_date["#attributes"]["datetime"] %}

Now we will do some comparisons to check if the event is a single day, muti-day, or a muti-day event that takes place in two different months.

First we will check for a single day event by comparing the start and end day.


{% if event_start_date | date('U') | format_date('custom', 'd') == event_end_date | date('U') | format_date('custom', 'd') %}

From the above code, we are doing a few things. First, we convert the UTC ISO formatted date into a Unix timestamp and then from there, we leverage a custom format to compare the event start and end day using Twig's comparison operator, ==.

If this is true, then we write the twig code as:


{# Check for and render a single day date. #}
  {{ event_start_date | date('U') | format_date('custom', 'M d Y') }}

... which will render like this Feb 29 2020. Next, we will add some logic if the event is muti-day and takes place within the same month:


{# If the start date month and end date month match. #}
{% elseif event_start_date | date('U') | format_date('custom', 'M') == event_end_date | date('U') | format_date('custom', 'M') %}
  {{ event_start_date | date('U') | format_date('custom', 'M d - ') }}
  {{ event_end_date | date('U') | format_date('custom', 'd Y') }}

For the above code, we once again use a comparison operator to check if the month start and end date is the same. If so, then the date renders within here as Jan 12 - 16 2020. Finally, if the event takes place within two different months, we add the end date month to our custom date formatter:


{# If the start date month and end date month DO NOT match. #}
    {% elseif event_start_date | date('U') | format_date('custom', 'M') != event_end_date | date('U') | format_date('custom', 'M') %}
      {{ event_start_date | date('U') | format_date('custom', 'M d - ') }}
      {{ event_end_date | date('U') | format_date('custom', 'M d Y') }}

This will render as Jan 29 - Feb 03 2020. And there you have it, nicely formatted custom event dates all within a Twig template.

The image above shows the finished event listingThe image above shows the finished event listing

Summary

I should note that Twig's custom date formats draw from PHP datetime so when you see something like format_date('custom', 'M d Y'), those time formats might seem familiar. Coding up dates in Twig in this manner was a great learning experiment for me. As with all things Drupal, there are so many different ways to achieve the same end result so I am sure there are no doubt other ways to do this. In fact, probably the Date and time UI within Drupal would probably suffice in most cases as you can create custom date formats in there as well. In the end, this was a great learning experience for me.

Resouces

Jul 23 2019
Jul 23

A common architectural model in Drupal is for one entity to reference another. For example, when using the Paragraphs module to build structured modular content, imagine that you have a paragraph type that references another paragraph type. The use case for this might be to build something like a grid of cards, an image gallery, or a carousel. The parent or "referencing paragraph" points to another paragraph type that contains an image, text, and title. Thus, the referencing paragraph is a container of multiple, similar sets of child entities that can be re-purposed in different manners depending on a setting in the referencing paragraph.

Use case

With this type of referenced entity paradigm, one can create a custom theme hook according to a setting in the referencing paragraph such as a select list with a set of "style options." In this way, we can theme with custom templates, HTML, and CSS depending on the style selection.

Getting started

The diagram below outlines our design pattern. In this, we see that the referencing paragraph has a select list, "Choose a Style" to select how the referenced entities will appear. The we see a visualization of "Cards" as one possible option.

This diagram outlines our architectural modelThis diagram outlines our architectural model

Essentially you would create a regular paragraph type with an image and a textarea, "Image & Text." Then, create another paragraph type, "Image & Text Reference" that points to Image & Text. This is visualized below.

The Paragraphs UI showing the relationship between the referencing and referenced paragraphsThe Paragraphs UI showing the relationship between the referencing and referenced paragraphs

Once that's all setup, add content and ensure that your local Drupal environment is setup for theming and development. I like to use Docksal for my local setup which makes it easy to get up and running with Xdebug. In addition, you'll want Twig debugging enabled in your local settings files. See the resources section below for more information.

Debugging with Twig & Xdebug

Now that we've added some content, we can start to debug. Inspecting the elements in Chrome, we see that our theme hook is named paragraph.






From the above information, we can leverage template_preprocess_paragraph for further debugging using Xdebug. In the case of creating a custom theme hook, we can use the Drupal 8 API's function hook_theme_suggestions_HOOK_alter where 'HOOK" is the discovered theme hook above; "paragraph." In this way, we can use theme_suggestions_paragraph_alter.

Using Paragraph's API function, getParentEntity(), we can run Xdebug in the referenced paragraph bundle and evaluate the expression, $paragraph->getParentEntity(). This "reaches" up to any values you'd like to retrieve from the referencing paragraph bundle and can make them available in the referenced paragraph bundle.

Xdebug in action showing getParentEntity() evaluatedXdebug in action showing getParentEntity() evaluated

From the above, we see the value that's been set on our select list, "Choose a Style." Now we can leverage that as we like in our referenced paragraph theme hook.

 if ($bundle === 'image_text') {
 // We evaluate the expression in Xdebug.
 // And from that, we set a variable and derive the value. ($host_value).
 $host_value = $paragraph->getParentEntity()->field_style->value;
 $suggestions[] = 'paragraph__' . $paragraph->bundle() . '__' . $view_mode . '__' . $host_value;
 $suggestions[] = 'paragraph__' . $paragraph->bundle() . '__' . $host_value;
    }

New theme suggestions

Once again, viewing Twig debug, we now have awesome new theme suggestions available.




Note the presence of:

   * paragraph--image-text--gallery.html.twig
   * paragraph--image-text--default--gallery.html.twig

These two new theme suggestions derive from the $host_value we had set in theme hook definition and creation.

Summary

This is really just the tip of the iceberg of what can be accomplished using getParentEntity() within paragraphs but it should come in really handy.

I'd also add that I've recently become a huge fan of Xdebug in combination with PHPStorm and it massively speeds up development and debugging. I used to use Kint, but no more, it was just too slow. It's really worth the time and effort to get up and running with Xdebug.

In addition, I started learning to use Adobe XD for the architectural diagram above. XD is an awesome free tool from Adobe that's very similar to the Sketch app.

Resources

May 10 2016
May 10

I've been theming with Drupal since version 6 back in 2009. In the intervening years, I've seen a lot of changes of how theming and front-end development is approached and have tried to keep up with the latest trends and adhere to best practices. Drupal 8 takes theming to a whole new level; it feels elegant, freeing, uniform, cohesive, and logical. So there's a lot to get excited about here.

In Drupal 8, there's templates for just about anything and with Drupal 8's new theme hooks, you can really narrow in on crafting a custom template for your specific use case. This makes getting at and altering markup far more accessible than it was in Drupal 6 and 7.

My preferred method for Drupal 8 theming is using Classy as my base theme, creating a sub-theme off of that and then overriding the parts that are necessary. An override is as simple as copying one of Classy's 100+ templates into your Classy based sub-theme. Sometimes, an out of the box template is not enough so you can create your own theme hook in that case.

In this post, I'll help you get started with creating Drupal 8 custom theme hook suggestions and templates. I also refer to various tools that I use, you can read more about them in my article, Drupal 8 Development: 5 Things to Add to Your Theming Toolbox.

Overview

The goal for this tutorial is to create a theme hook and subsequent template for a group of block regions in our site. The regions are called postscript_one, postscript_two, and postscript_three. The Drupal API function we will use for this is hook_theme_suggestions_HOOK_alter where "HOOK" is the name of the theme hook that we see in our Twig debug area. So essentially, we'll grab this theme hook and make a template suggestion for an array of these regions.

Getting Started

To start off, I'll place a block in one of my postscript regions and then examine the placed block with Web Inspector. Here is what I see when we do that:


From the above, we see the theme hook name, region as well as a two template suggestions. What I am after here is a common region template for any of the the three postscript regions that I mentioned earlier. Time to fire up Devel Kint and see what we can learn by inspecting the region array. For this, I will initiate my theme hook function in my theme's .theme file. Of note is hibiscus, my Classy sub-theme theme name and the HOOK, "region".

/\*\*

- Implements hook_theme_suggestions_HOOK_alter().
  \*/
  function hibiscus_theme_suggestions_region_alter(&$suggestions, $vars, $hook) {

}

Now I'll enable kint() as kint($vars);:

function hibiscus_theme_suggestions_region_alter(&$suggestions, $vars, $hook) {
  kint($vars);
}

After I run drupal cache:rebuild all using Drupal Console, I'll now see kint print out the region array on my page. As you can see from the below image.

Kint search in action

Construct the theme hook

Using Kint search, we can search for and copy the path for the region name definition, $var['elements']['#region'] It is from this that we will construct our array of regions and subsequent theme hook. First I define the region name variable from the above:

$region = $vars['elements']['#region'];

Now I'll define an array of my three regions:

$region_postscript = array(
'postscript_first',
'postscript_second',
'postscript_third',
);

Next I construct an if statement to make the actual theme suggestion:

if (in_array($region, $region_postscript)) {
$suggestions[] = 'region' . '\_\_' . 'postscript';
}

What this does is say, if it's any of the three postscript regions, create a theme hook that's common to all these regions.

Putting it all together

Putting the entire function all together, we now have:

/\*\*

- Implements hook_theme_suggestions_HOOK_alter().
  \*/
  function hibiscus_theme_suggestions_region_alter(&$suggestions, $vars, $hook) {

// Define a variable for the #region name.
$region = $vars['elements']['#region'];

// Define an array of our postscript regions.
$region_postscript = array(
'postscript_first',
'postscript_second',
'postscript_third',
);

// If we are in any of these regions, create a theme hook suggestion.
if (in_array($region, $region_postscript)) {
$suggestions[] = 'region' . '\_\_' . 'postscript';
}
}

Now that I have written my theme hook, I can clear cache and reload the page to see my new theme hook suggestion in action.


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