Need some help with your project? Contact me

Creating a custom Views filter in Drupal 8

In the previous article we've seen how we can interact programatically with Views in Drupal 8 in order to create a custom field in our Views results. Today, we will be looking a bit at how we can create a custom filter you can then add to the View in the UI and influence the results based on that.

Filters in Views have to do with the query being run by Views on the base table. Every filter plugin is responsible with adding various clauses in this query in an attempt to limit the results. Some (probably most) take on configuration parameters so you can specify in the UI how the filtering should be done.

If you remember from the last article, to create our field we extended the FieldPluginBase class. Similarly, for filters, there is a FilterPluginBase class that we can extend to create our own custom filter. Luckily though, Views also provides a bunch of plugins that extend the base one and which we can use or extend to make our lives even easier. For example, there is a BooleanOperator class that provides a lot of the functionality needed for this type of filter. Similarly, there is an InOperator class, a String class, etc. You can find them all inside the views/src/Plugin/views/filter directory of the Views core module or here.

In this tutorial, we will create 2 custom filters. One will be a very simple one that won't even require creating a new class. The second one will be slightly more complex and for which we will create our own plugin.

The code we write will go in the same module we started in the previous article and that can be found in this repository.

Node type filter

The first filter we will write is very simple. We want to be able to filter our node results by the machine name of the node type. By default, we can use a filter in which we select which node types to be included. Let's say, for the sake of argument, we want a more complex one, such as the one available for a regular text value like the title. The String class will be perfect for this and will provide actually 100% of our needs.

So let's go to our hook_views_data_alter() implementation and add a new filter:

    $data['node_field_data']['node_type_filter'] = array(
      'title' => t('Enhanced node type filter'),
      'filter' => array(
        'title' => t('Enhanced node type filter'),
        'help' => t('Provides a custom filter for nodes by their type.'),
        'field' => 'type',
        'id' => 'string'
      ),
    );

Since the table that we are interested in altering the query for is the node_field_data table, that is what we are extending with our new filter. Under the filter key we have some basic info + the id of the plugin used to perform this task. Since our needs are very simple, we can directly use the String plugin without us having to extend it. The most important thing here though is the field key (under filter). This is where we specify that our node_type_filter field (which is obviously a non-existent table column) should be treated as being the type column on the node_field_data table. So, by default, the query alter happens on that column. And this way we don't have to worry about anything else, the String plugin will take care of everything. If we didn't specify that, we would have to extend the plugin and make sure the query happens on the right column.

And that's it. You can clear your cache, create a View with nodes of multiple types and add the Enhanced node type filter to it. In its configuration you'll have many matching options such as equals, contains, does not contain etc you can use. For example, you can use contains and specify the letters art in order to return results whose node type machine name contain these letters.

Node title filter

The second custom filter we build will allow Views UI users to filter the node results by their title from a list of possibilities. In other words, they will have a list of checkboxes which will make it possible to include/exclude various node titles from the result set.

Like before, we need to declare our filter inside the hook_views_data_alter() implementation:

    $data['node_field_data']['nodes_titles'] = array(
      'title' => t('Node titles'),
      'filter' => array(
        'title' => t('Node titles'),
        'help' => t('Specify a list of titles a node can have.'),
        'field' => 'title',
        'id' => 'd8views_node_titles'
      ),
    );

Since we are filtering on the title column, we are extending again on the node_field_data table but with the title column as the real field to be used. Additionally, this time we are creating a plugin to handle the filtering identified as d8views_node_titles. Now it follows to create this class:

**src/Plugin/views/filter/NodeTitles.php:**

    <?php

    /**
     * @file
     * Definition of Drupal\d8views\Plugin\views\filter\NodeTitles.
     */

    namespace Drupal\d8views\Plugin\views\filter;

    use Drupal\views\Plugin\views\display\DisplayPluginBase;
    use Drupal\views\Plugin\views\filter\InOperator;
    use Drupal\views\ViewExecutable;

    /**
     * Filters by given list of node title options.
     *
     * @ingroup views_filter_handlers
     *
     * @ViewsFilter("d8views_node_titles")
     */
    class NodeTitles extends InOperator {

      /**
       * {@inheritdoc}
       */
      public function init(ViewExecutable $view, DisplayPluginBase $display, array &$options = NULL) {
        parent::init($view, $display, $options);
        $this->valueTitle = t('Allowed node titles');
        $this->definition['options callback'] = array($this, 'generateOptions');
      }

      /**
       * Override the query so that no filtering takes place if the user doesn't
       * select any options.
       */
      public function query() {
        if (!empty($this->value)) {
          parent::query();
        }
      }

      /**
       * Skip validation if no options have been chosen so we can use it as a
       * non-filter.
       */
      public function validate() {
        if (!empty($this->value)) {
          parent::validate();
        }
      }

      /**
       * Helper function that generates the options.
       * @return array
       */
      public function generateOptions() {
        // Array keys are used to compare with the table field values.
        return array(
          'my title' => 'my title',
          'another title' => 'another title',
        );
      }

    }

Since we want our filter to be of a type that allows users to select from a list of options to be included in the results, we are extending from the InOperator plugin. The class is identified with the @ViewsFilter("d8views_node_titles") annotation (the id we specified in the hook_views_data_alter() implementation).

Inside our plugin, we override three methods:

Inside init(), we specify the title of the set of filter options and the callback that generates the values for options. This callback has to be a callable and in this case we opted for the generateOptions() method on this class. The latter just returns an array of options to be presented for the users, the keys of which being used in the query alteration. Alternatively, we could have also directly created the options inside the init() method by filling up the $this->valueOptions property with our available titles. Using a callback is cleaner though as you can perform various logic in there responsible for delivering the necessary node titles.

The point of overriding the query() and validate() methods was to prevent a query and validation from happening in case the user created the filter without selecting any title. This way the filter has no effect on the results rather than returning 0 results. It's a simple preference meant to illustrate how you can override various functionality to tailor your plugins to fit your needs.

And that's it. You can add the Node titles filter and check the box next to the titles you want to allow in the results.

Conclusion

In this article we've looked at how we can create custom filters in Drupal 8 Views. We've seen what are the steps to achieve this and looked at a couple of the existing plugins that are used across the framework and which you can use as is or extend from.

The best way to learn how all these work is by studying the code in those plugin classes. You will see if they are enough for what you want to build or extending them makes sense. In the next article we are going to look at some other Views plugins, so stay tuned.

Further information on the topic?

Comments

Hello,

Thanks a lot for this tutorial, it was enlightening for me. In your example the first filter is a static one, the second is a dynamic one but manual (the user has to make a choice). I'm wondering if it is possible to make a dynamic and automatic filter like you did the dynamic manual one.

More precisely I'm wondering if your approach would be the good way to create a dependency between two fields a & b with the content of field b dependent of the user choice of the field a. The two fields are entity references and the two entities A & B are referencing a same third entity on which I count to filter the result of field b.

Sincerely,
Guillaume

I am trying to create a custom views filter that will add a new filter to determine the day of the week. Any recommendations on what I am doing wrong will be greatly appreciated. Below is what I have so far.


class todays_date_handler_filter_numeric extends NumericFilter { function operators(){ $operators = parent::operators(); $operators += array( 'todays date' =&gt; array( 'title' =&gt; $this-&gt;t('Is this day of the week'), 'short' =&gt; $this-&gt;t('todays date'), 'method' =&gt; 'op_day_of_week', 'values' =&gt; 0, ) ); return $operators; function op_day_of_week($field) { $this-&gt;query-&gt;addWhereExpression($this-&gt;options['group'], 'DATE_FORMAT(NOW(),"%w")'); } } }

I want to add new operator using NumericFilter But I cann't do Can you Provide me a simple example

I saw minor status error - "error missing group" status in the category when adding filter in Views UI. So i added a group property in field array in module_name.views.inc file.

$data['node__field_custom_date']['event_date'] = array(
    'title' =&gt; t('Custom Dates'),
    'group' =&gt; t('Content'),
    'filter' =&gt; array(
      'title' =&gt; t('Event Date - By Year'),
      'help' =&gt; t('Specify Event Dates By Year.'),
      'field' =&gt; 'field_event_date_value',
      'id' =&gt; 'custom_filter_event_year'
    ),
  );

This is a useful and great post; many thanks for it.
In your previous article (Creating a custom Views field in Drupal 8), you have created a field.
How can we adapt this tutorial to create a filter that will handle this field? (Just a simple "string" filter)
I have tried to do it myself, but I end up with an error when I am trying to use the filter:

Unknown column 'node.my_added_view_field' in 'where clause'

I'm having same problem as above. Trying to create custom filter for custom field. Getting column not found.

Thank you for the article.
How to implement a filter with a list of values? I found this class, but there are no examples of how to use it - https://api.drupal.org/api/drupal/core%21modules%21options%21src%21Plugi.... x
Thank you.

Hi,
Thanks a lot for this great post. I just wonder how it will be if we want to create custom filter for date.. Year for example.

Print year select list for and filter the content based on created field.
2017
2016
2015

I'm also interested in how you could get the dates (Y) for a specific content type so we can display it on the page and use it in the filter.

I'm also interested in to have date(Y) expose filter for views content list page.

One thing that needs to be done in addition to the above is to add the schema to your module. I had this bite me, where phpunit tests were failing due to schema validation issues.

This can basically be done by copying and modifying the views.filter.in_operator definition in views.filter.schema.yml:

views.filter.d8views_node_titles:
  type: views_filter
  label: 'Allowed node titles'
  mapping:
    operator:
      type: string
      label: 'Operator'
    value:
      type: sequence
      label: 'Values'
      sequence:
        type: string
        label: 'Value'
    expose:
      type: mapping
      label: 'Expose'
      mapping:
        reduce:
          type: boolean
          label: 'Reduce'
    group_info:
      mapping:
        group_items:
          sequence:
            type: views.filter.group_item.in_operator
            label: 'Group item'

Could you please let me know where to edit exactly in views.filter.schema.yml.
We cannot directly edit core file. need to keep this in different file. Please suggest where to add above code.

Add new comment

You can post comments in Markdown and basic HTML tags.
For code blocks, wrap your code within '~~~'. For example:
~~~
$var = 'my variable';
~~~