If you want the user to be able to filter search results in different ways, you can end up with a lot of exposed filters, which take up too much room on top of your query results. Wouldn't it be nice to be able to hide them into a (collapsed) fieldset?

When approached in the right way, it is pretty easy.
A straightforward example is mentioned in http://drupal.org/node/199991#comment-2159158 :
in views-exposed-form.tpl.php:
* create a fieldset
* put the rendered HTML for the exposed filter (which you already have) into the fieldset
* render the fieldset and output it.

The below example does more. But first, this:
Why use the theming layer?
Why not use hook_form_alter() to manipulate this form and add a fieldset there?

Answer: because views does it in the theming layer too.
The standard theming code for views exposed filters (i.e. template_preprocess_views_exposed_form() in .../modules/views/theme/theme.inc:553) renders separate form elements itself, with its own ordering mechanism. If you shuffle elements around (i.e. place them in a fieldset) in hook_form_alter(), this code will not be able to find things.
So we need to work with, or override, the code in the standard theme layer.

The example in above link (placing all the exposed views controls in a collapsed fieldset) does the override in the .tpl.php file.
The below example does it all in one theming function.

It puts the exposed filters in a fieldset only if the filter has no value filled. This way, if the user filtered the content (either through a URL part or using the 'submit' button), this fact is always visible.
This makes for the start of what could be regarded "faceted search" functionality for views results.

If you don't want to do this for all your exposed views, you need to call the function by something like this:

/**
 *  Theme function implementing theme_views_exposed_form().
 *
 *  Note we cannot use YOURTHEME_views_exposed_form() because we only want to handle the rendering
 *  of certain exposed views forms, and leave the other forms rendered by the default implementation.
 */
function YOURTHEME_views_exposed_form__VIEWNAME1($elements) {
  return _YOURTHEME_views_exposed_form($elements); // call the 'real' function (which you can rename however you want; this example prepends an underscore).
}
/**
 *  Theme function implementing theme_views_exposed_form().
 */
function YOURTHEME_views_exposed_form__VIEWNAME2($elements) {
  return _YOURTHEME_views_exposed_form($elements);
}

The function doing the work is below. It works out of the box for _most_ cases, but as you can see in the comments, some areas may need to be adjusted for e.g. certain special controls. This is 'code in progress', to be adjusted to your own needs.

/**
 * Theme function implementing theme_views_exposed_form().
 * Render an exposed form in a custom way.
 *
 * The regular theming functionality is done in template_preprocess_views_exposed_form()
 * and the views-exposed-form.tpl.php file. That functionality is combined here, and modified.
 */
function YOURTHEME_views_exposed_form($form) {
  views_add_css('views');

  $filters_with_values = '';
  $filters_without_values = '';
  // Put all single checkboxes together in the last spot.
  $checkboxes = '';

  foreach ($form['#info'] as $id => $info) {
    /* The way template_preprocess_views_exposed_form() works, is:
     * it calls drupal_render() on the 'operator' and 'value' widgets and stores
     * that rendered HTML in its own variables, for outputting that HTML in the
     * tpl.php later. That way, when $form is rendered later, these controls are
     * not output anymore; the widgets are 'taken out of the normal form
     * rendering order' (and rendered in the order in which they appear in
     * $info), without touching the $form structure.
     *
     * We will do the same thing... But generate the full HTML (with DIVs etc)
     * for a widget, as would be done in the .tpl.php. Afterward, we determine
     * where to output that HTML.
     */

    // Set aside checkboxes. [from template_preprocess_views_exposed_form()]
    if (isset($form[$info['value']]['#type']) && $form[$info['value']]['#type'] == 'checkbox') {
      $checkboxes .= drupal_render($form[$info['value']]);
      continue;
    }

    /// Determine whether an exposed filter has a value. We'll need this later.

    $element = $form[$info['value']];
    $hasvalue = TRUE; // default: do not put the element inside the fieldset.
    if (isset($element['#value'])) {
      switch ($element['#type']) {
        case 'select':
          // Dropdowns' have #value 'Aii' when "<Any>" is selected.
          // Multiselect boxes have #value === array() when nothing is selected.
          $hasvalue = (!empty($element['#value']) && $element['#value'] !== 'All');
          break;
        case 'nodereference_autocomplete':
          // For a nodereference, the value is hidden deep in the [name][name] element
          $hasvalue = !empty($element['name']['name']['#default_value']);
          break;
        default:
          $hasvalue = !empty($element['#value']);
      }
    }
    // As you see below, some filter elements need to be checked in a custom way.
    // So more code may need to be added later.
    //For number fields:
    // The filter may have 3 textfields: value / min / max.  Min/max are only used for the '(not) between' op.
    // If the operator is not exposed, only a subset of these textfields exist.
    // If the operator is exposed, all 3 textfields exist, along with a 'operator' element.
    // Only the textfields on which a certain operator depends, are visible and only they
    // need to be checked. (A value filled in an invisible textbox must have no effect.)
    elseif (isset($element['value']) && isset($element['min']) && isset($element['max'])) {
      $o = $form[$info['operator']]['#value']; // the operator (key, like '=', '>='. 'between')
      foreach (array('value', 'min', 'max') as $e) {
        // element name of operator is e.g. 'amount_op';
        // $element[$e]['#dependency'] contains arrays of dependent operator 
        // values, keyed by e.g. 'edit-amount-op'
        $key = 'edit-' . str_replace('_', '-', $info['operator']);
        if (isset($element[$e]['#dependency'][$key]) && in_array($o, $element[$e]['#dependency'][$key])) {
          // operator $o depends on this field; check it
          $hasvalue = !empty($element[$e]['#value']);
          if ($hasvalue) {
            break; // one dependent element having a value is enough
          }
        }
      }
    }
    elseif (isset($element['min']) && isset($element['max'])) {
      //  one dependent element having a value is enough
      $hasvalue = !empty($element['min']['#value']) || !empty($element['max']['#value']);
    }
    elseif (isset($element['value'])) {
      $hasvalue = !empty($element['value']['#value']);
    }

    //For location fields:
    // The 'search distance' has fields for postal_code/search_distance/search_units
    // If one of postal_code/search_distance is empty and the other is filled,
    // there will be zero search results and the 'cause' should be visible.
    elseif (isset($element['postal_code']) && isset($element['search_distance'])) {
      $hasvalue = (!empty($element['postal_code']['#value']) || !empty($element['search_distance']['#value']));
    }

    /// Example of theming customization for individual elements:
    // Autocomplete textboxes are long. When they're inside a fieldset, make
    // them a little smaller.
    if (!$hasvalue && $element['#type'] == 'nodereference_autocomplete') {
      $form[$info['value']]['name']['name']['#size'] = $element['name']['name']['#size'] - 2;
    }

    /// Now do the rendering work.

    $widget = "<div class=\"views-exposed-widget\">\n";
    if (!empty($info['label'])) {
      $widget .= '  <label for="' . $form[$info['value']]['#id'] . '">'
        . $info['label'] . "</label>\n";
    }
    if (!empty($info['operator'])) {
      $o = drupal_render($form[$info['operator']]);
      if (!empty($o)) {
        $widget .= '  <div class="views-operator">' . $o . "</div>\n";
      }
    }
    // (make sure to pass by reference, so #printed can be set => not $element)
    $widget .= '  <div class="views-widget">' . drupal_render($form[$info['value']])
      . "</div></div>\n";

    /// Determine where the output goes:
    // Place elements in a collapsed fieldset so they don't clutter the area above the view,
    // UNLESS the element has a value (i.e. the view is filtered on it) already,
    // i.e. you always want to see active filters.
    if ($hasvalue) {
      $filters_with_values .= $widget;
    }
    else {
      $filters_without_values .= $widget;
    }
  }
  // Wrap up all the checkboxes we set aside into a widget.
  if ($checkboxes) {
    $filters_with_values .= "<div class=\"views-exposed-widget\"><div class=\"views-widget\">\n"
      . $checkboxes . "</div></div>\n";
  }

  /// Construct final rendered output

  // Filters having values should always be visible
  $output = '<div class="views-exposed-form"><div class="views-exposed-widgets clear-block">'
    . $filters_with_values;
  if (empty($filters_with_values)) {
    // All filter elements are inside the collapsed fieldset, so there is no reason
    // for the submit button to be outside. Place button inside too.
    $filters_without_values .= "<div class=\"views-exposed-widget submit\">\n"
      . drupal_render($form['submit']) . "</div>\n";
  }
  if (!empty($filters_without_values)) {
    // Render by putting the HTML inside a collapsed fieldset.
    // Enclose the fieldset in a DIV for easier styling/positioning & give it an id,
    // and do not collapse the field at first:
    // Some fields (e.g. number filters) need to be visible in order to be post processed
    // by JavaScript (views/js/dependent.js).
    // AFTER that JavaScript has run, the fieldset can be collapsed:
    drupal_add_js("$(document).ready( function () { Drupal.toggleFieldset($('#views-exposed-collapse1 fieldset')); });", 'inline');

    $element = array(
      '#type' => 'fieldset',
      '#children' => $filters_without_values,
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#title' => 'Restrict results',
      '#prefix' => '<div id="views-exposed-collapse1" class="views-exposed-widgets-fieldset clear-block" style="clear: both; padding-top: 0.5em;">',
      '#suffix' => '</div>',);
      // Maybe get rid of style info? Oh well, it's custom code anyway...
    $output .= drupal_render($element); // pass byref
  }

  // Include the submit button (unless that's already been rendered above)
  // plus any other elements (that might have been added by hook_form_alter()).
  // The original tpl.php would put that output into a DIV, so we will too -
  // but with one class added for possible positioning.
  $o = drupal_render($form);
  if (!empty($o)) {
    $output .= "<div class=\"views-exposed-widget submit\">\n" . $o . "</div>\n";
  }
  $output .= "</div></div>\n";
  return $output;
}

Comments

alioso’s picture

Hi there. Thanks for this solution, look like exactly what i am looking for. However I am a little confused as for what file (and where) to override. Tried different ways but i wasn't successful at making this work.
Many Thanks

roderik’s picture

There probably is a file called template.php in the folder of the theme you are using.

Just copy the last big function into the template.php file (changing YOURTHEME_ to the name of your theme). And empty your theme cache.
It should work right away, without modification.
If it works perfectly for your exact site/needs right away... that's another question. But it might.

alioso’s picture

Thanks. I just went back to this today and made it work. Dunno why I couldn't in the first place.

I was just wondering if there would be an easy way to build a collapsible fieldset around each set of checkboxes, taking the checkboxes filter title as the fieldset legend, instead of putting all checkboxes within the same fieldset.

Also, i am using the exposedviewsformalter_form_alter function in a custom module and one of my select list gets trapped into the fieldset with the checkboxes, only after one checkbox is selected. Strange.

Many thanks

roderik’s picture

Sure, there should be a way to do checkboxes in separate fieldsets. That is - if you can read and understand the above code.

You could replace the line that reads

$checkboxes .= drupal_render($form[$info['value']]);

by

$tmp_html .= drupal_render($form[$info['value']]);
$element = array(
      '#type' => 'fieldset',
      '#children' => $tmp_html,
      '#collapsible' => TRUE,
      '#collapsed' => FALSE,
      '#title' => 'GET_THE_TITLE_FROM_WHEREEVER_IT_IS',
,);
$checkboxes .= drupal_render($element);

and maybe you should add a #prefix and #suffix to $element, just like you see in the $element array in the code up there. I'm not going to debug that; I just threw this together from the top of my head. Like I said, it's DIY code if you want to modify it to suit your needs.

alioso’s picture

a lot. that's great, and very useful. i just have to figure out the variable for the title, but that shouldn't be difficult.
Hope this will help others as well.

roderik’s picture

I was a bit too much in a hurry to remember all details correctly, but according to the Forms API reference it should be here:
... '#title' => $form[$info['value']]['#title'], ....

dgastudio’s picture

how to exclude for example "field_activities_value"??? I want to display it always.

roderik’s picture

Find the line that says "Determine where the output goes".

You can change the 'if ($hasvalue) { line below, to 'if ($hasvalue || $element['name'] == 'field_activities') {

...or something like that.

Grondhammar’s picture

Wow... a colleague of mine pointed this thread out a couple of days ago after I had asked around for simple CCK search solutions. Dropped the function in and it's more than I had hoped for. Great commenting too, this will be fun to tinker with. Thanks a ton for sharing it!

Raphael Apard’s picture

Just found this quick solution: http://dropbucket.org/node/220

Raphael

Pol’s picture