Skip to main content

Blog

Clayton speaks at events around the world on topics including Free Software, Platform Cooperatives and Cooperative Business Structures.

The Views module provides a flexible method for Drupal site builders to present data. On a recent project we needed to filter a view's result set in a way we could not achieve by means of the module's UI. How do you programmatically alter a view's result set before rendering? Let's see how to do it using the hooks provided by the module.

The need surfaced while working on the web site for MIT's Global Studies and Languages department, which uses Views to pull in data from a remote service and display it. The Views module provides a flexible method for Drupal site builders to present data. Most of the time you can configure your presentation needs through the UI using Views and Views-related contributed modules. Notwithstanding, sometimes you need to implement a specific requirement which is not available out of the box. Luckily, Views provides hooks to alter its behavior and results. Let’s see how to filter Views results before they are rendered.

Assume we have a website which aggregates book information from different sources. We store the book name, author, year of publication, and ISBN (International Standard Book Number). ISBNs are unique numerical book identifiers which can be 10 or 13 characters long. The last digit in either version is a verification number and the 13 character version has a 3-character prefix. The other numbers are the same. A book can have both versions. For example:

ISBN10:    1849511160
ISBN13: 9781849511162

In our example website, we only use one ISBN. If both versions are available, the 10-character version is discarded. We do this to prevent duplicate book entries which differ only in ISBN as shown in the following picture:

Screenshot of Views result

To remove the duplicate entries, follow this simple two step process:

  1. Find the correct Views hook to implement.
  2. Add the logic to remove unwanted results.

After reviewing the list of Views hooks, hook_views_pre_render is the one we are going to use to filter results before they are rendered. Now, let’s create a custom module to add the required logic. I have named my module views_alter_results so the hook implementation would look like this:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  // Custom code.
}

The ampersand in the function parameter indicates that the View object is passed by reference. Any change we make to the object will be kept. The View object has a results property. Using the devel module, we can use dsm($view->results) to have a quick look at the results.

Screenshot of array result.

Each element in the array is a node that will be displayed in the final output. If we expand one of them, we can see more information about the node. Let’s drill down into one of the results until we get to the ISBN.

Screenshot of expanded array result.

The output will vary depending on your configuration. In this example, we have created a Book content type and added an ISBN field. Before adding the logic to filter the unwanted results, we need to make sure that this logic will only be applied for the specific view and display we are targeting. By default, hook_views_pre_render will be executed for every view and display unless otherwise instructed. We can apply this restriction as follows:

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    // Custom code.
  }
}

Next, the logic to filter results.

/**
 * Implements hook_views_pre_render().
 */
function views_alter_results_views_pre_render(&$view) {
  if ($view->name == 'books'
    && $view->current_display == 'page_book_list') {
    $isbn10_books = array();
    $isbn13_books = array();
    $remove_books = array();

    foreach ($view->result as $index => $value) {
      $isbn = $value->field_field_isbn[0]['raw']['value'];
      if (strlen($isbn) === 10) {
        // [184951116]0.
        $isbn10_books[$index] = substr($isbn, 0, 9);
      }
      elseif (strlen($isbn) === 13) {
        // 978[184951116]2.
        $isbn13_books[$index] = substr($isbn, 3, 9);
      }
    }

    // Find books that have both ISBN10 and ISBN13 entries.
    $remove_books = array_intersect($isbn10_books, $isbn13_books);

    // Remove repeated books.
    foreach ($remove_books as $index => $value) {
      unset($view->result[$index]);
    }
  }
}

To filter the results we use unset on $view->result. After this process, the result property of the view object will look like this:

Screenshot of array after unset action.

And our view will display without duplicates book entries as seen here:

Screenshot of view after being overriden by code.

Before wrapping up, I’d like to share two modules that might help you achieve similar results: Views Merge Rows and Views Distinct. Every use case is different, if neither of these modules gets you where you want to be, you can leverage hook_views_pre_render to implement your custom requirements.

Update #1 Tue, 06/02/2015

As indicated by Leon and efpapado this approach only works for views that present all results in a single page. That was the original use case. The altering presented here only affects the current page and the pager will not work as expected.

 

Sign up if you want to know when Mauricio and Agaric give a migration training:

Practical individual resources for opting out of surveillance

Man's staring face besides the words "Big Brother is watching YOU"

Things you can do personally and without too much difficulty to get a little more free from Nineteen Eighty-Four style government oppression and surveillance capitalism.  (But remember, the most important and impactful things will require collective action!)

Sign up to be notified when Agaric gives a migration training:

When all goes according to plan—which is surprisingly often—theming in Drupal 8 is a straightforward matter of editing templates and stylesheets. We found things did not go according to plan when styling page-level action links (such as "Add new Forum topic") and content-level action links (or node links as Drupal 8 still calls them, such as "read more" or "add comment"). We are going to show how to add Bootstrap-specific styles, but the same approaches would be useful to add special stylings, button-like or otherwise, to selected action links and node links.

First, let's revisit the steps to follow when trying to add classes to Drupal-produced markup:

  1. Enable Twig debugging.
  2. Using your browser's inspector, identify the template providing the code you need to modify.
  3. Copy the template from where Twig debug output says it lives in Drupal core (or contrib) to your theme. If needed, use template name suggestions with a --modifier to selectively override the template in specific cases.
  4. Add BEM-compliant classes to your template. This is usually accomplished by tacking the addClass("here are--my-classes") method onto an attributes variable.
  5. Use the classes you added in your CSS.

Now let's try applying that to styling action links and node links (but only the actual link parts, not the surrounding text) as buttons.

Page-level action links

Here is an action link provided by Drupal core in the Forum module. It's classes don't align with Bootstrap's, so it displays without style.

A "Forums" headline with an unstyled "Add new Forum topic" action link below it.

The process

If we open the template that is providing the HTML for each link, menu-local-action.html.twig, it is only one line of code (and 12 lines of comments). It couldn't be simpler!

{#
/**
 * @file
 * Default theme implementation for a single local action link.
 *
 * Available variables:
 * - attributes: HTML attributes for the wrapper element.
 * - link: A rendered link element.
 *
 * @see template_preprocess_menu_local_action()
 *
 * @ingroup themeable
 */
#}
<li{{ attributes }}>{{ link }}</li>

Except... the attributes variable we have available is on the list item (li tag), not the link itself. Using this template we can't add classes to the already-rendered link element. Putting the button class on the list item would result in a common UX problem: button-looking elements with parts that are not clickable.

Even though this template cannot be used directly, it points us in the right direction. On line 10, a comments suggest us to see template_preprocess_menu_local_action(). So we shall.

A symbol finder in an IDE or grep will quickly take us to line 65 of core/includes/menu.inc:

/**
 * Prepares variables for single local action link templates.
 *
 * Default template: menu-local-action.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: A render element containing:
 *     - #link: A menu link array with 'title', 'url', and (optionally)
 *       'localized_options' keys.
 */
function template_preprocess_menu_local_action(&$variables) {
  $link = $variables['element']['#link'];
  $link += array(
    'localized_options' => array(),
  );
  $link['localized_options']['attributes']['class'][] = 'button';
  $link['localized_options']['attributes']['class'][] = 'button-action';
  $link['localized_options']['set_active_class'] = TRUE;

  $variables['link'] = array(
    '#type' => 'link',
    '#title' => $link['title'],
    '#options' => $link['localized_options'],
    '#url' => $link['url'],
  );
}

Here we can see exactly how Drupal is adding classes ('button' and 'button-action') to the buttons. Let's add our own preprocess function assuming our theme is name exampletheme:

  1. Add a function to our .theme file. In this example, the file would be exampletheme.theme.
  2. Name the function exampletheme_preprocess_menu_local_action(). That is, replace the word 'template' with the name of our theme name.
  3. Modify the $variables array to add our classes.

We could even remove the existing classes from the link, but we'll leave them for now. Note that the link that gets processed is $variables['link'] rather than $variables['element']['#link'].

The solution

/**
 * Extends template_preprocess_menu_local_action().
 *
 * Add Bootstrap button classes to a single local action link.
 *
 * Default template: menu-local-action.html.twig.
 *
 * @param array $variables
 *   An associative array containing:
 *   - element: A render element containing:
 *     - #link: A menu link array with 'title', 'url', and (optionally)
 *       'localized_options' keys.
 */
function exampletheme_preprocess_menu_local_action(&$variables) {
  $variables['link']['#options']['attributes']['class'][] = 'btn';
  $variables['link']['#options']['attributes']['class'][] = 'btn-success';
}

A forums listing page with a large green button labeled "Add new Forum topic" at the top.

Content-level action links

Next let's style node links as buttons. It's well-nigh impossible to get the btn and btn-success classes on the login and register links within the sentence "Log in or register to post comments". Therefore, we will use Bootstrap's handy mixins. The following is a SCSS code snippet which is turned into CSS by a SASS preprocessor.

.links--node a {
  @include button-variant($btn-success-color, $btn-success-bg, $btn-success-border);
  @include button-size($padding-base-vertical, $padding-base-horizontal, $font-size-base, $line-height-base, $btn-border-radius-base);
}

Finally, we just need to add the links--node class. Assuming our theme is called exampletheme:

/**
 * Implements hook_preprocess_links() for node entities.
 */
function exampletheme_preprocess_links__node(&$variables) {
  $variables['attributes']['class'][] = 'list-unstyled';
  $variables['attributes']['class'][] = 'links--node';
}

A comments section noting there are no comments yet, and providing two links, styled as buttons, in the sentence 'Log in or register to post comments'

Bonus: Link field links as buttons

For styling the output of link fields as buttons, the aptly-named Button link formatter module can help you out without the need for custom code nor templating.

Are you styling action links, node links, and other links?

Have you faced similar needs for changing the look of links, to be buttons or otherwise? How have you met them? Let us know in the comments!

You – our clients, colleagues, and crazed adoring fans – are the reason we do what we do. We would love to hear from you.

You have been successfully signed up to learn about data migration training opportunities.

CKEditor is well-known software with a big community behind it and it already has a ton of useful plugins ready to be used. It is the WYSIWYG text editor which ships with Drupal 8 core.

Unfortunately, the many plugins provided by the CKEditor community can't be used directly in the CKEditor that comes with Drupal 8. It is necessary to let Drupal know that we are going to add a new button to the CKEditor.

Why Drupal needs to know about our plugins

Drupal allows us to create different text formats, where depending on the role of the user (and so what text formats they have available) they can use different HTML tags in the content. Also, we can decide if the text format will use the CKEditor at all and, if it does, which buttons will be available for that text format.

That is why Drupal needs to know about any new button, so it can build the correct configuration per text format.

Adding a new button to CKEditor

We are going to add the Media Embed plugin, which adds a button to our editor that opens a dialog where you can paste an embed code from YouTube, Vimeo, and other providers of online video hosting.

First of all, let's create a new module which will contain the code of this new button, so inside the /modules/contrib/ folder let's create a folder called wysiwyg_mediaembed. (If you're not intending to share your module, you should put it in /modules/custom/— but please share your modules, especially ones making CKEditor plugins available to Drupal!)

cd modules/contrib/
mkdir wysiwyg_mediaembed

And inside let's create the info file: wysiwyg_mediaembed.info.yml

name: CKEditor Media Embed Button (wysiwyg_mediaembed)
type: module
description: "Adds the Media Embed Button plugin to CKEditor."
package: CKEditor
core: '8.x'
dependencies:
  - ckeditor

Adding this file will Drupal allows us to install the module, if you want to read more about how to create a custom module, you can read about it here.

Once we have our info file we just need to create a Drupal plugin which will give info to the CKEditor about this new plugin, we do that creating the following class:

touch src/Plugin/CkEditorPlugin/MediaEmbedButton.php

With this content:

namespace Drupal\wysiwyg_mediaembed\Plugin\CKEditorPlugin;

use Drupal\ckeditor\CKEditorPluginBase;
use Drupal\editor\Entity\Editor;

/**
 * Defines the "wysiwyg_mediaembed" plugin.
 *
 * @CKEditorPlugin(
 *   id = "mediaembed",
 *   label = @Translation("CKEditor Media Embed Button")
 * )
 */
class MediaEmbedButton extends CKEditorPluginBase {

  /**
   * Get path to library folder.
   * The path where the library is, usually all the libraries are
   * inside the '/libraries/' folder in the Drupal root.
   */
  public function getLibraryPath() {
    $path = '/libraries/mediaembed';
    return $path;
  }

  /**
   * {@inheritdoc}
   * Which other plugins require our plugin, in our case none.
   */
  public function getDependencies(Editor $editor) {
    return [];
  }

  /**
   * {@inheritdoc}
   * The path where CKEditor will look for our plugin.
   */
  public function getFile() {
    return $this->getLibraryPath() . '/plugin.js';
  }

  /**
   * {@inheritdoc}
   *
   *  We can provide extra configuration if our plugin requires
   *  it, in our case we no need it.
   */
  public function getConfig(Editor $editor) {
    return [];
  }

  /**
   * {@inheritdoc}
   * Where Drupal will look for the image of the button.
   */
  public function getButtons() {
    $path = $this->getLibraryPath();
    return [
      'MediaEmbed' => [
        'label' => $this->t('Media Embed'),
        'image' => $path . '/icons/mediaembed.png',
      ],
    ];
  }
}

The class's code is pretty straightforward: it is just a matter of letting Drupal know where the library is and where the button image is and that's it.

The rest is just download the library and put it in the correct place and activate the module. If all went ok we will see our new button in the Drupal Text Format Page (usually at: /admin/config/content/formats).

This module was ported because we needed it in a project, so if you want to know how this code looks all together, you can download the module from here.

Now that you know how to port a CKEditor plugin to Drupal 8 the next time you can save time using Drupal Console with the following command:

drupal generate:plugin:ckeditorbutton

What CKEditor plugin are you going to port?

Upcoming trainings

Modern Drupal is Drupal 8, 9, 10, 11 or above. It is the Drupal of today, with the ability to upgrade in place and powered in part by the Symfony framework. Modern Drupal uses the Twig templating engine. Modern Drupal will not leave your content behind.

Modern Drupal adds major improvements and deprecates old code gracefully.

Modern Drupal is what Agaric does.

Three illustrated faces.

Our relationship with technology is largely toxic- think Volkswagen cheating, Facebook spying, Uber being Uber. Tech is ruled by the elite and we are mostly at its mercy. As powerful movements have emerged challenging predatory power structures, let us do the same with technology.

Free/Open Source Software movements offer an alternative to corporate, predatory, proprietary technology. And yet Free Software still reflects many of these same oppressive relationships. One way to change that is with accountability.

Free Software means that anyone is free to read, use and remix the code that the software was written in. This helps with accountability because, unlike proprietary software, experts and community members can audit the code for security flaws and disingenuous functionality. However, there are several limitations with free software-

  1. Reviewing the code is only meaningful to coders.
  2. Changing the code or design can only be done by coders.

Only a small percentage of the world can code. An even smaller percentage have the time to write code for Free Software and an even smaller number have the time and expertise in any given project. This coder-centric framework also diminishes the many other skills essential to software: design, user research, project management, documentation, training, outreach to name a few.

As a major survey lead by GitHub supports (and comes as little surprise), the Free Software community is mostly white, male, cisgendered, financially well off, formally educated, able-bodied, straight, English speakers and citizens of Global North countries.

This means that the same groups of people designing and building proprietary software are also building Free Software. It means that despite its open licensing, the Free Software movement maintains the status quo of white supremacy, patriarchy and capitalism.

For Free Software to truly be free - to be free for anyone to build and use, we need to radically restructure our projects. It means building diverse communities where we are accountable to one another.

Many free software projects have already begun this work. Just a few examples-- Rust crafting and enforcing a thoughtful code of conduct, Ghost valuing design and user research throughout their work, Backdrop governing projects democratically and mentoring new contributors.

When we embody inclusion and accountability we grow vibrant communities building and using software that offers a clear alternative to the corporate, proprietary software; a software we can truly call free.

Overview

Learn to move content into Drupal 9 using the Migrate API. We will present an overview of the Extract-Transform-Load (ETL) pattern that Migrate implements. Source, process, and destination plugins will be explained to learn how each affect the migration process. By the end of the workshop, you will have a better understanding on how the migrate ecosystem works and the thought process required to plan and perform migrations. All examples will use YAML files to configure migrations. No PHP coding required.

There will be plenty of hands on examples to demonstrate different migrate concepts and how they can be used to import data into different types of fields. Time will also be allocated to answer attendee’s questions for topics not covered in the predefined material. 

Request a Private Training

Wendy is an Industrial Engineer turned web developer. She is an experienced WordPress engineer. Among her projects are online education platforms and multilingual sites. She volunteers at her local WordPress community and has presented at a couple of WordCamps.