Tag Archives: Drupal

Drupal: Sharing Inline Block Markup Between Custom Themes and Layout Builder Editing

Drupal 9’s layout builder system is a great improvement on how we’ve long been building content in Drupal and combined with custom block types is a sufficient replacement for modules like Paragraphs. But one of the frustrations I’ve run into while using it is that my custom theme styles get in the way of the editing UI, preventing editors from being able to access or read some of the controls they need to do their work.

Fortunately, the layout_builder_admin_theme module solves this problem and lets us use an admin theme like seven or claro when editing with layout builder, so all the editing controls appear where and as they should. However, my custom templates for inline blocks weren’t rendering with this enabled since I kept them in my custom theme directory, and my custom theme was no longer being used when editing.

Here’s how to move those templates into a custom module so that they’re loaded in both the site your visitors see as well as when editors are working with them.

First, create a custom module and add hook_theme to let Drupal know that you’ll be supplying templates for these blocks.

<?php

/**
 * Implements hook_theme().
 */
function MY_MODULE_theme() {
  return [
    'block__inline_block__MACHINE_NAME' => [
      'render element' => 'elements',
      'base hook' => 'block',
    ],
  ];
}

Now you can create a templates directory within your module and place a block--inline-block--MACHINE-NAME.html.twig template file there which will be picked up when the block is shown with your custom theme or the admin theme enabled.

{% if content %}
  {% set fields = content.content ? content.content : content %}
  <div{{ attributes.addClass(["block--MACHINE-NAME"]) }}>
    {{ title_prefix }}
    {{ title_suffix }}
    {% if content.actions %}
      {{ content.actions }}
    {% endif %}
    {% if fields.field_body %}
      <div class="block--MACHINE-NAME--body">{{ fields.field_body }}</div>
    {% endif %}
{% endif %}

The content object will work as normal when viewed in your custom theme, but when the admin theme is shown it will instead be an array with two items: content.content is the same object as content in the custom theme while content.actions contains some editing controls for positioning the block. To get around this inconsistency, we create a new variable, fields, which will have all the normal field content in it and then ensure that content.actions is printed out at the start of the block right after title_prefix and title_suffix which are also needed (along with attributes on a wrapper element) for the layout builder editing UI to work.

Importing Files into Drupal 8 media_entity Bundles

The media_entity module is Drupal 8 (and its eventual inclusion in core) is great for managing these assets so that they can be reused, have fields, and included in nodes with the paragraphs module. For a new project, we’re importing faculty profile photos from a remote database into Drupal and want to be able to track the files locally so that we can take advantage of all these features, plus image styles and responsive images.

I found several examples that covered parts of this, but no single example that did everything I needed it to do, so this is a write-up of what I ended up using to solve this problem.

First, I wanted to create a separate directory inside the files folder to hold the images that this module would manage. I would like to be able to use file_prepare_directory() for this task, but in testing I wasn’t able to get it to create a directory with sufficient permissions. Calling file_prepare_directory() later in my procedural code would return false. So instead I’m using FileSystem::mkdir() which lets me pass the mode. This is based on the install process from the Libraries API module.


<?php
/**
* @file
* Contains install, uninstall and update functions for Middlebury Profile Sync.
*/
/**
* Implements hook_install().
*/
function middlebury_profile_sync_install() {
$directory = file_default_scheme() . '://middlebury_profile_sync';
// This would ideally use file_prepare_directory(), but that doesn't create
// the directory with sufficient write permissions and a check against the
// function in the module code later fails.
if (!is_dir($directory)) {
$file_system = \Drupal::service('file_system');
$file_system->mkdir($directory, 0777);
}
}

Next, I use system_retrieve_file() to import the file from the remote URL, create a File object for it and track it in Drupal’s managed files. There are a lot of examples out there that would have you use the httpClient class manually to fetch the file, or use standard PHP file functions and create the File object by hand, but I found using this function to be much simpler for what I was trying to do.

I then created the Media bundle based on the File I had just created. This code is based on this gist example. Lastly, I assign the Media object’s id to the Featured Image field of my node and then save it. I found this example of assigning media ids to fields to be helpful.


<?php
$photo_dir = file_default_scheme() . '://middlebury_profile_sync';
if ($profile->hasPhoto() && file_prepare_directory($photo_dir)) {
$photo = $profile->getPhoto();
$id = $profile->getId();
$destination = $photo_dir . '/' . $id . '.' . pathinfo($photo, PATHINFO_EXTENSION);
$file = system_retrieve_file($photo, $destination, TRUE, FILE_EXISTS_REPLACE);
$media = Media::create([
'bundle' => 'image',
'uid' => \Drupal::currentUser()->id(),
'langcode' => \Drupal::languageManager()->getDefaultLanguage()->getId(),
'status' => Media::PUBLISHED,
'field_image' => [
'target_id' => $file->id(),
'alt' => t('Photo of ' . $node->title->value),
'title' => t('Photo of ' . $node->title->value),
],
]);
$media->save();
$node->field_featured_image = ['target_id' => $media->id()];
}
$node->save();

Displaying a date field in two formats in Drupal 8

Working on the conversion of our Museum of Art site, I ran into an issue with our “event” content type where we store the date and time of the event in a single datetime field, but want to display this information in two formats in different parts of the markup. In Drupal 7, we handled this using a preprocess function to access the data and set variables, but I wanted to find a more elegant solution in Drupal 8.

One option for this would be to have a, computed second field that stores the value again so it can be output twice in the node template and allow Drupal’s field API to format the output differently each time. I decided against this as it would require duplicating the data in the database.

Instead, I decided to format the date in the Twig template. Complicating this is that the date is stored as a string, rather than a timestamp in the database, so even if I tell the field API to format the output as a timestamp, there’s no way to directly access just that value without any of the markup.

To get around this, I set a variable in the Twig template to the value of the field piped to the date() function with the ‘U’ format, which will return an integer timestamp. This can then be passed into format_date() to have the date show up as desired.


{% set timestamp = node.field_event_time.value|date('U') %}
<article{{ attributes }}>
{% if label %}
<header>
<h1>{{ label }}</h1>
<h2>{{ timestamp|format_date('museum_date') }}
</header>
{% endif %}
<section class="contents">
<p>
{{ content.field_event_type }}<br />
<span class="label">Time</span>: {{ timestamp|format_date('museum_time') }}
<span class="label">Location</span>: {{ content.field_event_place }}<br />
</p>
{{ content }}
</section>
</article>

Always showing a teaser for lists of content in Drupal

For the main Middlebury and MIIS websites, we run a module named Monster Menus that adds hierarchical pages and permissions on top of Drupal. One of the effects of this is that we have many pages that are lists of nodes some of which we want to display in a full view, others of which we want to display as teasers. A common example is a list of news articles with a simple node stickied at the top. We want the news items to show up as teasers, allowing you to click through to see one of them in full view mode, but we also want that basic node at the top of the page to show up fully.

In Drupal 6 we achieved this by creating a custom template file outside of Drupal’s normal theme hooks and adding it to the template files array in the node preprocess function[ref]In order to make our preprocess functions a bit easier to read, we separate out the functionality for each content type into its own function using this method: [/ref] within template.php [ref]arg(1) is normally the node id when viewing a Drupal URL ending in /node/123. In Monster Menus, the node id is actually arg(3) since its URLs are of the form /mm/123/node/456 where the first number is the page id. I’ve used arg(1) in the examples to keep them familiar to most Drupal developers.[/ref].

<?php
function midd_preprocess_node_news(&$vars) {
if (arg(1) != $vars['nid']) {
$vars['template_files'][] = 'node-newslist';
}
}

Then we could have node-newslist.tpl.php along with node.tpl.php and show different markup. This all worked quite well in Drupal 6 because the CCK module exposed all the field data to the preprocess functions and you could manipulate it however you liked before outputting it. But it runs into trouble with Drupal 7’s field API and render arrays. You can still do it in Drupal 7, and just need to change “template_files” to “theme_hook_suggestions” in the example shown above, but I decided to go a different direction.

With Drupal 7’s new entity API, you can update how the render array is built based on the node’s metadata in hook_entity_view_mode_alter(). This has the advantage that the node now is rendered as a teaser and the fields set for that display mode in the admin interface show up as you’d normally expect them to, in the order defined, with the wrapper elements specified, and using the standard theme hook suggestion of node__TYPE__teaser.tpl.php.

<?php
/**
* Implements hook_entity_view_mode_alter().
*/
function midd_entity_view_mode_alter(&$view_mode, $context) {
if ($context['entity_type'] == 'node' &&
$context['entity']->nid != arg(1) &&
$context['entity']->type == 'news') {
$view_mode = 'teaser';
}
}