Creating Custom Views Filters With An Exposed Form Element In Drupal 6

Views is an amazing module, but sometimes you can come across certain limitations that make life difficult. For example, I created a view that pulled in a bunch of nodes based on different taxonomy terms. The problem was that I had more than one taxonomy term in different vocabularies being used to filter the results, which essentially caused the same field in the term_data table to be used for both taxonomy filters. So, no matter what I changed the parameters to I always received no results. I did try and add the second term as an argument but it isn't possible to do LIKE matches with views arguments.

After a bit of head scratching I decided that I needed to create a custom Views filter for the second element. This would create a second join between the node and the term_data tables and allow me to filter it using a LIKE match. Using this approach made sense as I also wanted the element to be exposed so that a user could enter a search term to restrict the View results. What follows is how to go about setting up custom Views filters along with the associated exposed form elements by using the Views API.

The first thing we need is a implementation of HOOK_views_api() so that Views knows that the module is Views enabled. These function hooks need to be added to a module so you can either use an existing module or create a new one.


/**
 * Implentation of HOO_views_api()
 *
 * @return array An associative array of views options.
 */
function mymodule_views_api() {
    return array(
        'api' => 2.0
    );
}

Before we can point to our Views filter handlers we must first let Views know where they are by including a HOOK_views_handlers() hook. Here, we are defining a single handler class that will make the needed changes to our query object and add in the form element. Note that the path is set to a directory called views in the current module.


/**
 *
 * Register the views handlers with the view.
 *
 * @return array An associative array of options.
 */
function mymodule_views_handlers() {
    return array(
        'info' => array(
            'path' => drupal_get_path('module', 'mymodule') . '/views',
        ),
        'handlers' => array(
            'mymodule_special_data_filter_view' => array(
                'parent' => 'views_handler_filter',
            ),
        ),
    );
}

The next thing we need is an implementation of HOOK_views_data(), which will tells Views about all sorts of things, but in this case we will tell Views about our filter. The function should return an associative array of items, each one being a filter that we want to implement. The array can be very large and complex, with table and field definitions, but to keep things simple I have tied in this filter to the node table by using the 'node' element of the $data array. This filter will therefore appear when we go on to create a node View type and will appear in the list of node filters.

The important bit here is the filter element (which tells Views to define a filter) and the filter handler (which will handle what our filter does). The handler element contains the name of a class we created a reference to earlier that will add the extra joins and where clauses to the query.


/**
 * Implementation of HOOK_views_data
 *
 * Tells views about the filters we need to add.
 *
 * @return array An associative array of options.
 */
function mymodule_views_data() {
    $data = array();

    $data['node']['mymodule_special_data'] = array(
        'title' => t('Custom Taxonomy Lookup'),
        'help' => t('Added to create a second link between the node and taxonomy tables.'),
        'filter' => array(
            'handler' => 'mymodule_special_data_filter_view',
        ),
    );

    return $data;
}

Create a file called mymodule_special_data_filter_view.inc in your module directory in the views sub folder and define a class called mymodule_special_data_filter_view. This class extends views_handler_filter and defines two methods. The value_form() method is used to create the element for the exposed form widget. The query() method is used to add (or remove) parts of the query in the View.


ensure_my_table();
        
        // Rest of query information goes here
    }
}

Essentially, what we are trying to add to the query are the following clauses.

A left join between the term_node and the node tables, using the alias term_node2 for the term_node table.

LEFT JOIN term_node term_node2 ON node.vid = term_node2.vid

A left join between the term_data and the term_data tables, using the alias term_data2 for the term_data table.

LEFT JOIN term_data term_data2 ON term_node2.tid = term_data2.tid

An addition to the WHERE clause to restrict the data being included.

AND (UPPER(term_data2.name) LIKE UPPER('%Term Name%'))

Although it took me a while to actually track down this information, the easiest part of this is creating the form element for the exposed filter. All you need to do is add a method called value_form() and then define the form elements you want to have in your exposed form. The following will create a single text field that we can use to extract our data from.


class mymodule_special_data_filter_view extends views_handler_filter
{
    /**
     * Shortcut to display the exposed options form.
     */
    function value_form(&$form, &$form_state) {
        //$options = menu_parent_options(menu_get_menus(FALSE), 1);

        $form['value'] = array(
            '#type' => 'textfield',
            '#title' => t('Term Name'),
            '#default_value' => NULL,
        );

        return $form;
    }
    
... SNIP

The final part of this is to add the code to the query() method to change the outputted SQL. I have included lots of comments in the code to clearly explain what is going on. Essentially, if the exposed form has been entered by the user then the value is picked up by this code and the needed joins are added. I also found that I had to add a call to $this->query->add_table() in order to create an alias for the term_node table. If this is left out then the second join is done between term_data2 and term_node, which leaves us back at square one.


    function query() {
        // Ensure table alias has been set
        $table_alias = $this->ensure_my_table();

        // Get term value
        $term_value = check_plain($this->view->exposed_input['mymodule_special_data_filter_view']);

        // Make sure term value is set.
        if ($term_value != '') {
            /*
            Query parts to add:
             1. LEFT JOIN term_node term_node2 ON node.vid = term_node2.vid
             2. LEFT JOIN term_data term_data2 ON term_node2.tid = term_data2.tid
             3. AND (UPPER(term_data2.name) LIKE UPPER('%Term Name%'))
             */

            // Add the term_node table with an alias of term_node2 this ensures 
            $this->query->add_table('term_node', NULL, NULL, 'term_node2');

            // Add a join between the term_node and the node tables
            // This uses the alias term_node2 for the term_node table
            $join = new views_join();
            $join->construct('term_node', $this->table_alias, 'vid', 'vid', array(), 'LEFT');
            $this->query->ensure_table('term_node2', $this->relationship, $join);

            // Add a join between the term_data and term_node tables
            // This uses the alias term_data2 for the term_data table
            $join = new views_join();
            $join->construct('term_data', 'term_node2', 'tid', 'tid', array(), 'LEFT');
            $return = $this->query->ensure_table('term_data2', $this->relationship, $join);

            // Add a where clause to the query
            $this->query->add_where($this->options['group'], "(UPPER(term_data2.name) LIKE UPPER('%%%s%%')) ", $term_value);
        }
    }

I have added a check here to make sure that a value exists from the exposed filter for me to use, if it is then I do the query altering, otherwise the code is not run. This might be different for your implementation so you need to check what is going on around this line:

$term_value = check_plain($this->view->exposed_input['mymodule_special_data_filter_view']);

I should note that this code was created specifically for my needs and I include it here as it took me a while to find out about all the needed components. Your Drupal project will almost certainly be different from mine so this code might not do what you want.

?>

Comments

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?

Permalink

I'm not sure about that. When I created this view I added the exposed filters in the same way as I normally do and the vews-exposed-form.tpl.php template seems to contain what it should do. Button should only contain the rendered submit button that is created by Views interanlly.

Name
Philip Norton
Permalink

Thanks philipnorton42 for quick repply. It seems to be working now.

Permalink

I have defined 3 custome filters. 1st holds checkboxes, 2nd holds radio button. For these two filters query function is getting executed and where clause is getting appended. But the 3rd one have textfield and select field, whe trying to submit query function doesn't executed. I tried to debug by echoing the things inside query function but no effect. Could you please tell me what could be wrong?

Permalink

I think problem is where filter holds more than one form element. If i define my filter with single text field with following code spec


$key = $this->options['expose']['identifier'];
$form[$key] = array(
'#type' => 'textfield',
'#size' => 15,
'#default_value' => $this->value,
);

It works fine. But if I want to include another form field, then If I changed above code spec to following query function is not getting executed.


$key = $this->options['expose']['identifier'];
$form[$key]['code'] = array(
'#type' => 'textfield',
'#size' => 15,
'#default_value' => $this->value,
);
$form[$key]['cat'] = array(
'#type' => 'select',
'#title' => 'Range',
'#options' => array(
'a' => t('A'),
'b' => t('B'),
'c' => t('C'),
'd' => t('D'),
),
'#default_value' => 'a',
);
Permalink

This is one of the best tutorials I've ever seen! Thanks so much. One thing I tweaked when I transliterated your code into my files is the line in the quer() function

$term_value = check_plain($this->view->exposed_input['mymodule_special_data_filter_view']);

I changed mine to:

$term_value = check_plain($this->view->exposed_input['mymodule_special_data']);

I'm looking forward to reading your other posts. Awesome job.

Permalink

I'm having a similar problem. It seems that a $form['value'] form element is required in order for a value_form with 2+ elements to be submitted correctly. At least when I add a value form element (for a total of 3 form elements in one value_form) my query function get's executed.


function value_form(&$form, &$form_state) {
    
    $form['cb_season'] = array(
      '#type' => 'select',
      '#title' => t('Season'),
      '#options' => $this->cb_season_options,
    );  

    $form['cb_year'] = array(
      '#type' => 'select',
      '#title' => t('Year'),
      '#options' => $this->cb_year_options,   
    );
    
    $form['value'] = array(
      '#type' => 'hidden',
      '#value' => '',
    );
  }

That said, I'm not sure how to get the user input for my other two form elements. When I use the devel modules dpm function to dump the contents of $this in the value_form, I only see the contents of the value form element but not the other two. The same is true when I dump the contents of the form_state variable...

I'll post back if/when I figure out how to access the values of the other form elements but I thought this might at least help others who are struggling with the same problem.

Permalink

I'm having a similar problem. It seems that a $form['value'] form element is required in order for a value_form with 2+ elements to be submitted correctly. At least when I add a value form element (for a total of 3 form elements in one value_form) my query function get's executed.


function value_form(&$form, &$form_state) {
    
    $form['cb_season'] = array(
      '#type' => 'select',
      '#title' => t('Season'),
      '#options' => $this->cb_season_options,
    );  

    $form['cb_year'] = array(
      '#type' => 'select',
      '#title' => t('Year'),
      '#options' => $this->cb_year_options,   
    );
    
    $form['value'] = array(
      '#type' => 'hidden',
      '#value' => '',
    );
  }

That said, I'm not sure how to get the user input for my other two form elements. When I use the devel modules dpm function to dump the contents of $this in the value_form, I only see the contents of the value form element but not the other two. The same is true when I dump the contents of the form_state variable...

I'll post back if/when I figure out how to access the values of the other form elements but I thought this might at least help others who are struggling with the same problem.

Permalink

Okay, after a bit more experimenting it appears that you can define a $form['value'] empty hidden form element in order to ensure the query function is executed. 


function value_form(&$form, &$form_state) {
  
    $form['cb_season'] = array(
      '#type' => 'select',
      '#title' => t('Season'),
      '#options' => $this->cb_season_options,
      //sets the default value based on the value from the options_form if this exposed form has not been submitted
      '#default_value' => (isset($form_state['input']['cb_season'])) ? $form_state['input']['cb_season'] : $this->options['cb_season'],
    );  

    $form['cb_year'] = array(
      '#type' => 'select',
      '#title' => t('Year'),
      '#options' => $this->cb_year_options,   
      //sets the default value based on the value from the options_form if this exposed form has not been submitted
      '#default_value' => (isset($form_state['input']['cb_year'])) ? $form_state['input']['cb_year'] : $this->options['cb_year'],
    );
    
    // required for the query function to be executed
    $form['value'] = array(
      '#type' => 'hidden',
      '#value' => '',
    );
  }

Then in the query function, your additional value_form form elements can be accessed using $this->view->exposed_input[<form element="" name="">] (ie: $this->view->exposed_input['cb_year']).


function query () {
 
    // set variables to be used in modifying the query
    // if the user submitted the exposed form then these values will be used
    // otherwise the value from the options_form will be used
    // assumes that the name of the form element in the options_form is the same as in the values_form
    $cb_year = (isset($this->view->exposed_input['cb_year'])) ? $this->view->exposed_input['cb_year'] : $this->options['cb_year'];
    $cb_season = (isset($this->view->exposed_input['cb_season'])) ? $this->view->exposed_input['cb_season'] : $this->options['cb_season'];

     // modify your query using the values submitted by the user (or the admin if user didn't apply filters) here
  }

Hope this helps someone!

Permalink

in function mymodule_views_handlers()

defined

mymodule_handler_filter_view

but in function mymodule_views_data() {

there is 'handler' => 'mymodule_special_data_filter_view',

is it right? or am I missing something?

 

Permalink

hey guys,

   I am getting a Error: handler for node > ms_customs_special_data doesnt exist. but I have it link to the correct class name. Im not sure what im getting, any help?

// in my ms_customs.module file

  function ms_customs_views_data() {
  $data = array();

  $data['node']['ms_customs_special_data'] = array(
    'title' => t('Custom Date filter'),
    'help' => t('Filter any views based on date ad term'),
    'filter' => array(
      'handler' => 'ms_customs_special_data_filter_view'
    ), 
  ); 
  return $data;
}

// in my ms_customs_special_data_filter_view.inc is my

class ms_customs_special_data_filter_view extends views_handler_filter

Permalink

This is a great article. I am pretty much impressed with your good work. You put really very helpful information. Keep it up.

Permalink

Hi,

thank you for you great tutorial!

Anyway I'm pretty sure there is a mistake in mymodule_views_handlers().

You declare a handler with the wrong name. It should be "mymodule_special_data_filter_view", not "mymodule_handler_filter_view".

Greetings.

-ermannob

Permalink
Views internal workings have always been kind of a secret magic for me, but your article helped me a lot! By the way, ermannob is right in mentioning that the handler key declared in hool_views_handlers() should be the same as the one in hook_views_data(). Great info, thanks.
Permalink
Hello! I've been reading your site for a while now and finally got the bravery to go ahead and give you a shout out from Dallas Tx! Just wanted to say keep up the fantastic job!
Permalink
Helped me a lot too. thanks.
Permalink

I am getting this error Broken/missing handler in mymodule.module

function mymodule_views_api() { return array( 'api' => 2, 'path' => drupal_get_path('module', 'mymodule') . '/views', ); }

mymodule.views.inc file in "views" folder

function mymodule_views_handlers() { return array( 'info' => array( 'path' => drupal_get_path('module', 'mymodule') . '/views', ), 'handlers' => array( 'mymodule_handler_filter_province_multiple' => array( 'parent' => 'views_handler_filter', ), ), ); }

i upload mymodule_handler_filter_province_multiple.inc file in "views" folder mymodule_handler_filter_province_multiple.inc class mymodule_handler_filter_province_mulitple extends views_handler_filter

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
5 + 7 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.