You are here

Creating custom filters with drupal and views

Ah views. Yes, the views module for Drupal is incredibly powerful and combined with CCK has essentially eliminated the need to ever create custom modules for content. While this is a happy thing, sometimes views just doesn't quite do what you want. And, rather sadly, there is almost no documentation on how to create custom views plugins/styles/filters/etc. Boo.

For a recent project, I had the need to have a view filtered by a single dropdown menu that could contain either taxonomy terms or date parameters. While drupal supports exposed filters for both of these things, it does not support having them both in one select menu. My initial attempt was to just create a block that through some voodoo found and submitted the view with arguments for the view. This almost worked, but tended to screw up ajax pagination, and was rather fragile to begin with. So, I embarked on the path of determining how one might go about creating a truly custom filter for a view.

Step 1 - Create your module

custom_filter.module

<?php
function custom_filter_views_api() {
  return array(
   
'api' => 2,
   
'path' => drupal_get_path('module', 'custom_filter') . '/inc'
 
);
}
?>

And that's about it... All this really says is my module targets views version 2, and you can find the files in the 'inc' directory of my module folder.

Step 2 - Module Definitions

Next we need to tell views about what our module provides.

inc/custom_filter.views.inc

<?php
/**
* Implementation of hook_views_handlers() to register all of the basic handlers
* views uses.
*/
function custom_filter_views_handlers() {
  return array(
    'info' => array(
      'path' => drupal_get_path('module', 'custom_filter') . '/inc', // path to view files
    ),
    'handlers' => array(
      // register our custom filter, with the class/file name and parent class
      'custom_filters_filter_multiple' => array(
        'parent' => 'views_handler_filter',
      )
    ),
  );
}

The above definition essentially tells views what our module does. In this case we simply give it the path to our files, and register one handler named 'custom_filters_filter_multiple. The parent parameter tells views which class our handler extends. In this case, I just extended the root filter class.

Next we need to provide a views_data() hook, which tells views a bit of information about our filter.

inc/custom_filter.views.inc

function custom_filter_views_data() {
  $data = array();
 
  // The flexible date filter.
  $data['node']['custom_filter'] = array(
    'group' => t('Custom'),
    'real field'  => 'custom_filter',
    'title' => t('Custom Date/Term combined filter'),
    'help' => t('Filter any Views based on date and term'),
    'filter' => array(
      'handler' => 'custom_filters_filter_multiple'
    ),
  );
 
  return $data;
}

In this case we return an array with our filter data. We give it a group (the title of which appears in the drop down to filter, uh, filters when you add a new filter to your view). We give it a real field, which in this instance does not actually exist since we're not filtering on a single real field. The title and help are displayed in the views UI, and the handler again points to our class for implementing this filter. Views seems to like redundency...

Step 3 - Filter Definition

Now it's time to actually create our filter. We create a file which contains our class, both matching the names we provided above.

inc/custom_filters_filter_multiple.inc

<?php

class custom_filters_filter_multiple extends views_handler_filter {
 
  /* this method is used to create the options form for the Views UI when creating a view
   * we use the standard drupal form api to return a form array, with the settings
   * we want to capture.
   */
  function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);

    // Step 1: fetch all our vocabularies, and build an array of options
    $terms = taxonomy_get_vocabularies();
    $show = array();
    foreach($terms as $term) {
      $show[$term->vid] = $term->name;
    }
   
    // Step 2: create a select field to choose Vocabulary options
    //   this allows you to choose which vocabulary to fetch terms for in the exposed filter
    $form['filter_vocab'] = array(
      '#type' => 'select',
      '#title'  => t('Vocabulary'),
      '#options'  => $show,
      '#default_value'  => $this->options['filter_vocab']
    );
   
    // Step 3: Create a checkbox field to select whether date options should be included
    $form['include_dates'] = array(
      '#type' => 'checkbox',
      '#title'  => t('Include Date Filters'),
      '#default_value'  => $this->options['include_dates']
    );
  }
 
  /* I'll be perfectly honest, I have no idea if this is required or not. I *think* it may be
   * as it defines our filter field. However I don't use it. I added it when trying to get
   * things working...
   */
  function value_form(&$form, &$form_state) {
    $form['custom_filter']  = array(
      '#type' => 'textfield'
    );
  }
 
  /* A custom display for our exposed form. Views normally uses the value_form for this
   * however we're skipping that entirely since we want our exposed form to be a completely
   * different beast. It's entirely possible I could move this to value_form however...
   */
  function exposed_form(&$form, &$form_state) {
    // for my use case the filtering is controlled by javascript, which
    // submits the form and also handles a date select popup.
    drupal_add_js(drupal_get_path('module','custom_filter') . '/custom_filter.js');
   
    // if we're displaying date options, add them to the options for our output filter
    if ( $this->options['include_dates'] ) {
      $display = array(
        'all'               => t('Show all'),
        'last-four-weeks'   => t('Last four weeks'),
        'last-three-months' => t('Last three months'),
        'last-six-months'   => t('Last six months'),
        'last-year'         => t('Last year'),
        'advanced'          => t('Advanced'),
      );   
    } else {
      $display = array('all'  => t('Show all'));
    }
   
    // get the terms for our configured vocabulary and add them to the options
    // for my case, terms are only 1 level deep, will need changes
    // if you have nested terms.
    if ( $this->options['filter_vocab'] ) {
      $terms = taxonomy_get_tree($this->options['filter_vocab']);
     
      foreach( $terms as $term ) {
        $display[$term->tid] = $term->name;
      }
    }
   
    // now create our select element with the chosen options
    $form['custom_filter'] = array(
      '#type' => 'select',
      '#title'  => t('Browse By'),
      '#options'  => $display
    );
  }

  // the query method is responsible for actually running our exposed filter
  function query() {
    // make sure our base table is included in the query.
    // base table for this is node so it may be redundent...
    $this->ensure_my_table();

    // make sure term node is joined in if needed
    // not exactly optimal since we may not need it if we're filtering by date
    $this->query->add_table('term_node');
   
    // get the value of the submitted filter
    $value = $this->value[0];

    // a bit ugly. Since we have date and taxonomy options we need to do a switch to exhaust
    // the date options before we can assume it's a taxonomy term
    switch( $value ) {
      case 'all';
        return;
      case 'last-four-weeks':
        $this->query->add_where($this->options['group'], "node.created > %s", strtotime('4 weeks ago'));
        break;
      case 'last-three-months':
        $this->query->add_where($this->options['group'], "node.created > %s", strtotime('3 months ago'));
        break;
      case 'last-six-months':
        $this->query->add_where($this->options['group'], "node.created > %s", strtotime('6 months ago'));
        break;
      case 'last-year':
        $this->query->add_where($this->options['group'], "node.created > %s", strtotime('1 year ago'));
        break;
      default:
        if ( is_numeric($value) ) {
          $this->query->add_where($this->options['group'], "term_node.tid = %d", $value);
        }
    }
  }
}

Hopefully that all makes sense. I'm not entirely sure it's the proper way to do it, but it works well and does what I need!

Comments

Hello, awesome article you've wrote. There are only couple of moment missed as I think:
1. custom_filter.js - this file is not presented at the article, but looks like its handling something with filter form
2. Your comments system has no email or some else subscription for following :)

The tutorial is pretty well written, so far one of the best I've seen in regards to Views in the Hive.
Just noting a minor nitpick here: for some reason only the first snippet of code has colored PHP syntax, the rest of the snippets have black foreground.

Great tutorial.
How to theme such exposed custom filter? I have added custom filters using above technique. Now I am trying to theme this exposed filter. So I have copied views-exposed-form.tpl.php, but some how in foreach($widgets as $id => $widget) --- $widget doesn't hold this exposed filter instead it gets printed in <?php print $button ?>. Could you please help me to theme filter?

Is this applicable to Views 3? just wondering. :)

Any chance to create a views 3 version?

Could you do a view 3 version of that?

Very nice guide to creating custom filters. And this works too.

Hey! nice example. thanks It works me for as excepted thank you very much. :)

Hi,

I've followed the entyre tutorial, but when I want to add the filter I'm getting this error:
The handler for this item is broken or missing and cannot be used. If a module provided the handler and was disabled, re-enabling the module may restore it. Otherwise, you should probably delete this item.

Any idea what I did wrong?

Thanks

Thanks for this tutorial. It works perfectly unless I check the box to remember the last setting the user selected. Then, I get this error: An illegal choice has been detected. Please contact the site administrator.

How can I resolve this. It is important that this filter setting is remembered when the page is returned to.

"Hey! nice example. thanks It works me for as excepted thank you very much. :)"
Nicely put. With thanks..
Kudos, Great stuff!

nice tutorial thanks for sharing

That was really helpful mate. I was searching for this all over internet. I could not find it anywhere except here. Our company recently moved to Drupal from WordPress because of some server related issues. It was tough times for us. But you people helped us a lot. Check our website.
http://www.omnitechsupport.com/blog

The distributions offer the benefit of another Drupal site without needing to physically search out and install third party contributed modules or adjust configuration settings.
http://pathology.residencypersonalstatements.net/how-to-compose-an-effec...

Appreciate it for this short training. The item operates properly except if My <a href="https://www.passbeemedia.com/">passbook</a> partner and i check this package to keep in mind the final setting the person determined. Next, My partner and i have this miscalculation: The illegal choice have been recognized. Remember to make contact with the web page administrator.

Innovation in payments is driven forward on a daily basis by tens of thousands of product managers and developers in the European payments industry seeking to design the most competitive offerings to consumers and corporate customers.
VOLVO Impact EPC http://www.autoepc4u.com/volvo-impact-trucks-buses-lorry-spare-parts-cat...

Add new comment

Plain text

  • No HTML tags allowed.
  • Web page addresses and e-mail addresses turn into links automatically.
  • Lines and paragraphs break automatically.
© 2014 chadcf