I'm trying to create a custom node search view, that takes Taxonomy terms as a filter (dropdown - autocomplete not an option). The problem I have, is that there could be >1000 terms and I would like to restrict the dropdown to show only the taxonomy terms that are already in use by the nodes returned in the initial view results. Does this make sense?

Can anyone advise if this is possible, and if so, how best to achieve it?

Thanks for the help!

Comments

dawehner’s picture

as far as i know, you cannot do this, the filter is build before the view itself

merlinofchaos’s picture

Status: Active » Closed (won't fix)

dereine is correct.

mr.andrey’s picture

Status: Closed (won't fix) » Active

I'm actually looking for a similar solution, but I don't have 1000s of terms (only 15 or so).

I wonder if there's a workaround, maybe to temporarily store the tids of the results somewhere and just use jquery to hide the empty options from the dropdown?

Any ideas?

I can write a little extension/module to accomplish this, as it's something that I think many would find mighty useful.

Best,
Andrey.

dawehner’s picture

You could use

<?php
function hook_views_pre_view(&$view) {
  drupal_add_js(array('view_result' => $view->result), 'setting');
}
?>

This pushes all results into js, which can be quite a lot of data.

mr.andrey’s picture

That comes up as an empty array, even though I can see one result in a view. Is there a reference file somewhere on the whole $view variable/object?

I think these are the steps:
1. Get the tids of all of the results, and load them into an array
array(1,2,3,6)

2. Get the filter tids and load the ones that don't match the result tids into an array
array(4,5)

3. Remove these options from $view's exposed filters in the preprocess.

Thanks for the fast reply. I think this is pretty doable.

The whole $view variable/object seems a bit overwhelming to me at the moment. It has so much in it. I essentially just need to get the results and their tids for a specific vocab, and override the exposed filters for that particular vocab. Any help with this would be much appreciated.

Andrey.

mr.andrey’s picture

Here's what I have so far:

function HOOK_form_views_exposed_form_alter(&$form, $form_state) {
  if ($form['#id'] == 'views-exposed-form-my-subscriptions-default') {
	$affected_exposed_filters = array('tid', 'tid_1', 'tid_2'); // set your filter names here
	if (is_array($form['#parameters'][1]['view']->exposed_input)) {
	  foreach ($form['#parameters'][1]['view']->exposed_input as $fid => $tid) {
		if (in_array($fid, $affected_exposed_filters)) {
		  $vid = $form['#parameters'][1]['view']->filter[$fid]->options['vid'];
		  dpm('fid:'.$fid.' vid:'.$vid.' tid:'.$tid); 
		}
	  }		
	}
  }
}

We now have the vocab id and the term that it selected for each exposed taxonomy filter.

To unset an exposed filter value, we can use:

unset($form['tid']['#options'][8]);

From here I can:
* Set up view arguments to correspond with each of the exposed taxonomy filters
* Get the view results, passing exposed filter values as arguments
* Get the distinct term ids that show up in the results for each vocabulary, and unset the ones that are not used on each of the exposed filters.

Does this make sense? Any other way of getting this done?

Andrey.

mr.andrey’s picture

Nevermind the last post, I decided to scratch that and do it another way:

function hook_form_views_exposed_form_alter(&$form, $form_state) {
  if ($form['#id'] == 'views-exposed-form-my-subscriptions-default') { 
	// prevent recursion - only execute once
	static $called = 0;
	if ($called == 1) {
	  return;
	}
	$called = 1; // flag as called
	
	$affected_exposed_filters = array('tid', 'tid_1', 'tid_2'); // SET YOUR FILTER NAMES HERE
	
	// get results
	$view = views_get_view('my_subscriptions');
	$view->init_display();
	$view->pre_execute();
	$view->execute();
	
	// assemble results into a comma-separated nid list
	foreach($view->result as $row) {
	  $nids[] = $row->nid;
	}
	$nids = implode(',', $nids);
	
	// get the list of used terms
	$used_tids = db_result(db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {term_node} tn WHERE tn.nid IN (%s)", $nids));
	$used_tids = explode(',', $used_tids);
	
	// unset the unused term options
	foreach($affected_exposed_filters as $filter) {
	  foreach($form[$filter]['#options'] as $tid => $tname) {
		if ($tid != 'All' && !in_array($tid,$used_tids)) {
		  unset($form[$filter]['#options'][$tid]);
		}
	  }
	}
  }
}

This is much more flexible, as it only relies on the filter names and the tids derived from the final view result.

I think there's a more efficient way of doing the whole unsetting thing, but I can't think of it at the moment.

I'll fine tune it later, but this works pretty well as far as I could tell. Only the relevant (non-0-result) terms show up in the exposed filters, and progressively disappear as the choices narrow down.

IMO, this (or an improved version of it) should be part of views, a little on/off checkbox next to the exposed taxonomy term filter.

Best,
Andrey.

citrustree’s picture

This is great, just what I'm looking for!

I can't figure out how to implement this though - what do I do with it?

Thank you.

mr.andrey’s picture

You need to create a custom module and replace the hook with your module name. Alternately you can try using template.php and replace hook with phptemplate, but I haven't tested that. The weight of the module might be important (my helper module weight is 9999). Read up on creating modules, it's not too hard: http://drupal.org/node/206753. I have a helper module with all sorts of custom alterations. Most things you can use template.php for, but some things need to be in a module.

Here's an updated version:

function hook_form_views_exposed_form_alter(&$form, $form_state) {
  // which views this affects
  if ($form['#id'] == 'views-exposed-form-my-subscriptions-default'
   || $form['#id'] == 'views-exposed-form-my-purchased-default'
   || $form['#id'] == 'views-exposed-form-my-faves-default'
   || $form['#id'] == 'views-exposed-form-terms-page-1'
  ) { 
	// prevent recursion - only execute once
	static $called = 0;
	if ($called === $form['#id']) {
	  return;
	}
	$called = $form['#id']; // flag as called
	
	$affected_exposed_filters = array('tid', 'tid_1', 'tid_2'); // SET YOUR FILTER NAMES HERE
	
	// get results
	$view = views_get_current_view();
	$view->init_display();
	$view->execute();
	
	// assemble results into a comma-separated nid list
	foreach($view->result as $row) {
	  $nids[] = $row->nid;
	}
	$nids = implode(',', $nids);
	
	// get the list of used terms
	$used_tids = db_result(db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {term_node} tn WHERE tn.nid IN (%s)", $nids));
	$used_tids = explode(',', $used_tids);
	
	// unset the unused term options
	foreach($affected_exposed_filters as $filter) {
	  
	  foreach($form[$filter]['#options'] as $tid => $tname) {
		if ($tid != 'All' && !in_array($tid,$used_tids)) {
		  unset($form[$filter]['#options'][$tid]);
		}
	  }
	  // show a message if filtered
	  $selected_tid = $form['#parameters'][1]['view']->exposed_input[$filter];
	  if ($selected_tid && $selected_tid != 'All') {
		$selected_terms[] = $form[$filter]['#options'][$selected_tid];
	  }
	}
	
	if (is_array($selected_terms)) {
	  $form['submit']['#suffix'] = "<span style='font-size:12px'>only showing results for <b><i>".implode(', ', $selected_terms)."</i></b></span>";
	}
	
  }
}
ppmax’s picture

I did something similar using two funcs in template.php.

This func grabs all terms:

function eirmachinery_ppterms($node) {
	$vocabularies = taxonomy_get_vocabularies();
	foreach($vocabularies as $vocabulary) {
		if ($vocabularies) {
			$terms = taxonomy_node_get_terms_by_vocabulary($node, $vocabulary->vid);
			if ($terms) {
				foreach ($terms as $term) {
					// print_r($terms);
					$vocname = $vocabulary->name;
					$keyword = $term->name;
					// $termlinks[] = array($vocname => $keyword);
					// $termlinks[] = $keyword;
					$terms_array[$vocname] = $keyword;
				}
			}
		}
	}
	return $terms_array;
}

this func takes the current view, calls the function above, then returns an array that gets called by theme_links (eirmachinery_return_years(). In the views header I call it by print theme_links(eirmachinery_return_years(), array('class' => 'mini-menu"));

function eirmachinery_return_years() {
	$view = views_get_current_view();
	$years = array();
	foreach($view->result as $item) {
		$node = node_load($item->nid);
		$categories = eirmachinery_ppterms($node);
		$years[] = $categories['Year'];
	}

	//count array vals and remove dupes
	$years = array_count_values($years);

	//create links
	$path = arg();
	if($years) {
		foreach($years as $year => $value) {
			$links[$year] = array(
				'title' => $year . "($value)",
				'href'  => 'products/all/' . $year
			);
		}
	}
	//debug
	// drupal_set_message("<pre>" . var_export($years, TRUE) . "</pre>");
	// drupal_set_message("<pre>" . var_export($path, TRUE) . "</pre>");
	if($links) {krsort($links);}
	return $links;
}
citrustree’s picture

Superb, thank you mr.andrey, I really appreciate your time. I did have to do one thing slightly different to the suggestion. Rather than using the ids for the terms such as tid_1 I needed to use the actual names, such as 'price' and. 'region'. I'm going to attempt to build a little backend for this now to allow the settings to applied by site - wish me luck! I'll post it if I work it out.

One more question - I needed to use the #id views-exposed-form-search-page-1 in the first if operator, however I'd prefer this to apply to any page of the view. It does actually appear to work on all pages even thogh the #id has page-1 at the end - is this the case?

Thank you again.

mr.andrey’s picture

@cirtustree,

The "tid", "tid_1" and "tid_2" are filter identifiers. You can set them if you like, but if you don't, they just default to tid, tid_1, etc.

Yes, the $form['#id'] applies only to one page in a view. If you want to apply it to an entire view, you can use $form['#parameters'][1]['view']->name.

Best,
Andrey.

citrustree’s picture

Thanks mr.andrey great stuff.

Where you have multiple exposed filters it's still possible to select a combination of options which returns no records.

EG if node1 has price=500 and region=london, and node2 has price=1000 and region=nottingham. If 500 and nottingham are selected from the exposed filters no nodes apply.

I got round this with a little javascript. The only alternative as far as I'm aware would be a stepped search which is too complex for my example (something like faceted search converted into pulldowns).

Anyway, if anyone is interested the jQuery I used was as follows:

	<?php if (arg(1) == 'search') { ?>
	<script language="javascript">
		$(document).ready(function(){ 
			$("#edit-submit").hide();
			$("#formsearch select").change(function() {
		    		$("#formsearch").submit();
			});
		}); 
	</script>
	<?php } ?>
  • Place the fragment into your head on page.tpl.php;
  • Edit the first line to ensure your jQuery appears in the correct context;
  • edit-submit on line 4 should hide your submit button;
  • the following 2 lines add a function to the select tags in the form to submit the form when any single option is changed. Make sure #formsearch on both lines is changed to reflect the id of your form.

As you can tell from above this simply ensures the form is submitted with each selection so it's nearly impossible to get zero results. If javascript is disabled it degrades back to the button version which is fine, but can return zero results.

mr.andrey’s picture

@citrustree,

Good idea. I overlooked that someone may select multiple filters at the same time.

I've added this to my code with a few changes.

I've noticed that if I hide the submit the button via JS, it appears again after the view reloaded. I'm using ajax to sumit views and I'm assuming you're doing a page reload, so it works for you just fine. I have multiple views on a page, so reloading the whole page doesn't make sense for me. I added a line to turn it off manually. You can replace that with a JS line if you like for a more graceful degradation to non-JS browsers.

function hook_form_views_exposed_form_alter(&$form, $form_state) {
  if ($form['#id'] == 'views-exposed-form-my-subscriptions-default'
   || $form['#id'] == 'views-exposed-form-my-purchased-default'
   || $form['#id'] == 'views-exposed-form-my-faves-default'
   || $form['#id'] == 'views-exposed-form-terms-page-1'
  ) { 
	// prevent recursion - only execute once
	static $called = 0;
	if ($called === $form['#id']) {
	  return;
	}
	$called = $form['#id']; // flag as called
	
	$affected_exposed_filters = array('tid', 'tid_1', 'tid_2'); // SET YOUR FILTER NAMES HERE
	
	// get results
	$view = views_get_current_view();
	$view->init_display();
	//$view->pre_execute(); // <- skip pre_execute as it limites results to current page
	$view->execute();
	
	// assemble results into a comma-separated nid list
	foreach($view->result as $row) {
	  $nids[] = $row->nid;
	}
	$nids = implode(',', $nids);
	
	// get the list of used terms
	$used_tids = db_result(db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {term_node} tn WHERE tn.nid IN (%s)", $nids));
	$used_tids = explode(',', $used_tids);
	
	// unset the unused term options
	foreach($affected_exposed_filters as $filter) {
	  
	  foreach($form[$filter]['#options'] as $tid => $tname) {
		if ($tid != 'All' && !in_array($tid,$used_tids)) {
		  unset($form[$filter]['#options'][$tid]);
		}
	  }
	  // show a message if filtered
	  $selected_tid = $form['#parameters'][1]['view']->exposed_input[$filter];
	  if ($selected_tid && $selected_tid != 'All') {
		$selected_terms[] = $form[$filter]['#options'][$selected_tid];
	  }
	}
	
	// display the filtered message
	if (is_array($selected_terms)) {
	  $form['#suffix'] = "<span style='font-size:12px'>only showing results for <b><i>".implode(', ', $selected_terms)."</i></b></span>";
	}
		
	// manually hide the submit button
	$form['submit']['#attributes'] = array('style' => 'display:none');
	
	// submit form on selection change - avoids empty results
	drupal_add_js("$(document).ready(function(){
		$('#".$form['#id']." select').change(function() {
		  $('#".$form['submit']['#id']."').submit();
		});
	});", 'inline'); 
	
  }
}

And speaking of JS, what are the browsers that don't support it that people actually use (not including w3c and lynx)? Here's a breakdown of visitors to one of my sites and as far as I know they all support JS. I haven't been doing much non-JS optimization. Is there a reason for it at all?

1. Firefox 825 41.73%
2. Safari 575 29.08%
3. Internet Explorer 446 22.56%
4. Chrome 101 5.11%
5. Mozilla 13 0.66%
6. Opera 10 0.51%
7. Mozilla Compatible Agent 3 0.15%
8. Camino 1 0.05%
9. Googlebot 1 0.05%
10. Konqueror 1 0.05%
11. Opera Mini 1 0.05%

Best,
Andrey.

UPDATE: Hmmm.. using Ajax, the first time the form submits fine, but after that the selection change doesn't do anything. Need to look deeper into it. Any ideas?

citrustree’s picture

Ooh, that's clever that you got the module to show the JS!

// manually hide the submit button
$form['submit']['#attributes'] = array('style' => 'display:none');

I wouldn't hide the button with CSS - we should fix the JS. If CSS is enabled but JS is disabled that means your select options don't have the JS bound to them, but the button will still hide. IE it will break your form.

I've noticed that if I hide the submit the button via JS, it appears again after the view reloaded.

I didn't notice that as I was not using AJAX, but that makes sense. I'm no jQuery/JS expert, but I believe JS does not automatically bind/rebind to elements which are new upon the AJAX page load, but instead they need to rebound. The form elements probably need rebound after the page reloads as they are effectively new elements.

There is a handler available which takes the pain out of this - http://docs.jquery.com/Events/live (requires jQuery 3.4!!) - however I've not used that so do not understand the detail. Another option is the liveQuery plugin http://docs.jquery.com/Plugins/livequery which I believe would work with the Drupal shipped version of jQuery.

The alternative (and the way I've done it before) is to manually rebind the elements after the AJAX page load. The code we used to hide the button and attach the submit function to the selects could be created as a function instead of running directly on page load, then instead run the function both after pageload and also on AJAX updates. This would update the code to something like:

	<script language="javascript">
		$(document).ready(function(){
			updateForm();
		});		
		function updateForm(){
			$("#edit-submit").hide();
			$("#formid select").change(function() {
		    	$("#formid").submit();
			});
		}			
	</script>

...however each AJAX call made by Views would then need to run updateForm() after the callback - and I'm not sure how that could be done without hacking the module. Do you know?

UPDATE: and to answer your question about the amount of browsers using JS - I don't know. I only ever use JS when it will degrade gracefully - IE when the site will still be usable without it. This is the reason I would only hide the button with JS - since then the JS is loaded so the selects will work. If we can get my suggestion above working then we don't need to worry about browsers not having JS installed. Does AJAX in Views degrade gracefully with JS disabled (I assume it does)? I tried to test it however telling the view to use AJAX does not seem to have made any difference... hmm...

mr.andrey’s picture

I looked into live(), and it seems to be just the thing to use. It needs jQuery 1.3, which I use via jQuery Update module on most of my D6 setups, so it's not a problem. Now the follow-up select changes work just fine.

I do agree that display:none is not the best way to hide the submit button, but at the moment I'm leaving it as that because I'm not sure how to prevent it from popping up after each ajax submit. Is there a way embed execution of a JS function into the form element via Drupal form API? After the form loads, the hide button function gets called, or something like that.

Thanks for your work on this, it's coming together pretty nice.

function hook_form_views_exposed_form_alter(&$form, $form_state) {
  if ($form['#id'] == 'views-exposed-form-my-subscriptions-default'
   || $form['#id'] == 'views-exposed-form-my-purchased-default'
   || $form['#id'] == 'views-exposed-form-my-faves-default'
   || $form['#id'] == 'views-exposed-form-terms-page-1'
  ) { 
	// prevent recursion - only execute once
	static $called = 0;
	if ($called === $form['#id']) {
	  return;
	}
	$called = $form['#id']; // flag as called
	
	$affected_exposed_filters = array('tid', 'tid_1', 'tid_2'); // SET YOUR FILTER NAMES HERE
	
	// get results
	$view = views_get_current_view();
	$view->init_display();
	//$view->pre_execute(); // <- skip pre_execute as it limites results to current page
	$view->execute();
	
	// assemble results into a comma-separated nid list
	foreach($view->result as $row) {
	  $nids[] = $row->nid;
	}
	$nids = implode(',', $nids);
	
	// get the list of used terms
	$used_tids = db_result(db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {term_node} tn WHERE tn.nid IN (%s)", $nids));
	$used_tids = explode(',', $used_tids);
	
	// unset the unused term options
	foreach($affected_exposed_filters as $filter) {
	  
	  foreach($form[$filter]['#options'] as $tid => $tname) {
		if ($tid != 'All' && !in_array($tid,$used_tids)) {
		  unset($form[$filter]['#options'][$tid]);
		}
	  }
	  // show a message if filtered
	  $selected_tid = $form['#parameters'][1]['view']->exposed_input[$filter];
	  if ($selected_tid && $selected_tid != 'All') {
		$selected_terms[] = $form[$filter]['#options'][$selected_tid];
	  }
	}
	
	// display the filtered message
	if (is_array($selected_terms)) {
	  $form['#suffix'] = "<span style='font-size:12px'>only showing results for <b><i>".implode(', ', $selected_terms)."</i></b></span>";
	}
	
	// manually hide the submit button
	$form['submit']['#attributes'] = array('style' => 'display:none');
	
	// submit form on selection change - avoids empty results
	drupal_add_js("$(document).ready(function(){
		$('#".$form['#id']." select').live('change', function() {
		  $('#".$form['submit']['#id']."').submit();
		});
	});", 'inline'); 
	
  }
}



Here's a Step-By-Step Howto with a screenshot of how it looks. I will keep it updated.



citrustree’s picture

Good stuff. I'd not had time to test either way yet. I tried jQuery update, however the minute it was installed EVERY link on the site tried to open in lightbox, very weird.

Anyway, the whole point in live is that it should work with the button too - IE live should be used on $("#edit-submit").hide(); also to make sure that hides when the form updates... Does that not work?

Can I see what site you have this on? Send me a message if you like - I'll forward you a link to mine too.

troybthompson’s picture

This is exactly what I was looking for. Have you verified that this works using template.php? I tried following the step by step how to and it doesn't seem to get triggered. The only change is to make hook_form_views_exposed_form_alter into views_form_views_exposed_form_alter, correct?

mr.andrey’s picture

You also need to specify the view and filter names. I have it broken down by lines in my how-to.

trevorwh’s picture

This gets triggered for me, but the code isn't working as expected.

For some reason, it is as if the SQL query isn't working - it removes all the values.

In addition to that, it doesn't load all pages, just the current page worth of values.

Ideas?

ManyNancy’s picture

This sounds worthy of being a real module?

Bilmar’s picture

subscribing - this sounds like a great feature and I will be trying the HowTo this weekend.
thanks!

birchy82’s picture

I can't seem to get this to work in my template.php file, Using a zen sub theme all up to date modules.

Here is the code I have, I'll show only the area you said to alter on your how-to instructions:

function views_form_views_exposed_form_alter(&$form, $form_state) {
if ($form['#id'] == 'catalog_listing'
) {
// prevent recursion - only execute once
static $called = 0;
if ($called === $form['#id']) {
return;
}
$called = $form['#id']; // flag as called
$affected_exposed_filters = array('tid'); // SET YOUR FILTER NAMES HERE

rak’s picture

If the view has an argument, when the select is submitted, the argument is lost, since the form's action is "/". I did a childish workaround to keep the argument, which also was a taxonomy term, by adding these lines after $view = views_get_current_view();

---------------------
$old_arg = $view->args[0];
$form['#action'] = '/taxonomy/term/'.$old_arg;
---------------------

This is just a 'for now fix', when you got time, a normal implementation would be needed. Thanks for the great work you did so far.

--
RAk

mr.andrey’s picture

Birchy, the "catalog_listing" should probably be "catalog-listing". Views converts underscores to dashes. Try printing $form['#id'] and see what it shows you.

drupal_set_message($form['#id']);

Best,
Andrey.

P.S. As per previous post, yes, this probably should be a module in it's own right, but unfortunately I don't have the time to maintain anything beyond a simple how-to.

mr.andrey’s picture

@rak,

Argument settings is part of $view->pre_execute. Unfortunately, it also limits the results to the first page, so we can't just call it.

  function pre_execute($args = array()) {
    $this->old_view[] = views_get_current_view();
    views_set_current_view($this);
    $display_id = $this->current_display;

    // Let modules modify the view just prior to executing it.
    foreach (module_implements('views_pre_view') as $module) {
      $function = $module . '_views_pre_view';
      $function($this, $display_id, $args);
    }

    // Prepare the view with the information we have, but only if we were
    // passed arguments, as they may have been set previously.
    if ($args) {
      $this->set_arguments($args);
    }

//    $this->attach_displays();

    // Allow the display handler to set up for execution
    $this->display_handler->pre_execute();
  }

So you basically need to call $view->set_arguments() right after $view->init_display(); You can get arguments from $form and add them into the set_arguments() call.

Good luck and let me know how it goes.

Sorry that I don't have time to test this myself, but it shouldn't be too hard.

Best,
Andrey.

lordkirin’s picture

I think I figured it out try this. Using the Faceted Search Module point to my view. The key is two // behind your base path set in faceted search. Needs space after the 25

Example:

catalog//results/taxonomy:25

mghatiya’s picture

Hello,

I need to know solution to this problem. I followed the thread a bit, but not sure if it has been fully solved as yet. Please let me know if it has been made into a patch or module or some such.

I would like to subscribe to the thread, but don't see any option to do so. Maybe this post will help me in keeping track.

Thanks,
Mukesh

alexkessler’s picture

Version: 6.x-2.5 » 6.x-3.x-dev

I would also appreciate such a feature.
A simple checkbox there you can choose if a term has an associated node would be awesome.

Above snippets didn't work for me (only the terms of the current page appeared), so
I ended with a sql query with some joins to make it work.

Let's set this to 6.x-3.x-dev and cross fingers...

intyms’s picture

my two cents:
If you want it to be a feature request, then change the category and title of this issue.

mr.andrey’s picture

Version: 6.x-3.x-dev » 6.x-2.5

This is working fine in 6.x.2.5. You can follow the steps here or just read through the early part of the thread.

I haven't tested it on 6.x.3.x, as it's still alpha. If in the future my clients switch to that version, I'll post a new patch. You can try adapting it yourself as well. It's probably not that hard, depending on how much views have changed from 2 to 3.

I'm switching it back to 2.5 for now, since there hasn't been any development on 3.

Best,
Andrey.

Rich Hewer’s picture

Title: How do I limit a taxonomy dropdown exposed filter to the terms used by nodes in view results? » Limited to first page?

Dear Andrey,

Thanks for your nice code, it seems to be working (almost) perfectly. However, I understand from reading here that skipping pre_execute() is supposed to allow all of the view's results to be included, but it does not. In my hands I am still getting only the terms used on the current page of results.

After seeing that I thought this was by design, but then reading the code and seeing this comment made me think otherwise:
// <- skip pre_execute as it limites results to current page

Is there a solution for this? I assume that I must have made some error somewhere, as you report that this code is working.

Thanks,
Rich

EDIT: didn't read properly, I've changed the title of the whole thing, please change back!

intyms’s picture

Title: Limited to first page? » How do I limit a taxonomy dropdown exposed filter to the terms used by nodes in view results?

changing the title

feuillet’s picture

Subscribing. Would love to have this feature as a checkbox on the filter-settings.

Rich Hewer’s picture

Thinking about it, could this be because I have put this in as a module, and it hasn't integrated properly, allowing pre_execute to still run? Wondering what is different about how I've set this up. Can this not be done in a module?

mr.andrey’s picture

I just realized that it might not be working for some of you because of jQuery version. I just downgraded my jQuery to Drupal default for some testing, and it made this not work. Try getting jQuery Update with jQuery 1.3.2

Best,
Andrey.

Rich Hewer’s picture

I went and got jQuery Update 6.x-2.x-dev and enabled it, but this didn't make any difference, I still only get the results from view first page.

robby.smith’s picture

subscribing

dawehner’s picture

Joachim created a module for exact this thing: http://drupal.org/project/views_taxonomy_selective_filter

volocuga’s picture

This is GREAT improvement!

Hope it works!!!

Thanks!

kasiawaka’s picture

This is a great module. It fixed my issue with the taxonomy terms that are bilingual - I wanted to expose the terms to the user but only show English terms on the English pages and French on French pages. Because I am using the taxonomy set as "Translation mode: Per language terms", that module is perfect to do it. See more details here: http://drupal.org/node/285494#comment-2818888

Thanks a lot!

dawehner’s picture

Status: Active » Fixed

So this is fixed.

msumme’s picture

try hierarchical select http://drupal.org/project/hierarchical_select

It has a nice configuration page for the views taxonomy that can limit the terms to ones with an associated node, and it seems to work really well.

If you have a single-level taxonomy, you can still use it.

jday’s picture

how do you limit the terms to the ones with associated nodes with hierarchial_select? I'm still getting all terms with that module enabled and I don't see a setting for 'active terms' or however it's done, can anyone enlighten me?

using the code in #16 I get the error:
Fatal error: Call to a member function init_display() on a non-object

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

mr.andrey’s picture

I tried the Hierarchical Select, but it gave me a fatal error (http://drupal.org/node/792092) when I tried to use the "Display the node count" or "Require associated node" features.

Meanwhile, I wrote another tutorial, this time using taxonomy with depth and much better code:

How to show only used terms in Views exposed filters with depth.

It works really well and takes care of the basic two needed features:
* Only display terms that have nodes associated with them.
* Update the view on term selection

Best,
Andrey.

eL’s picture

Version: 6.x-2.5 » 6.x-2.11
Status: Closed (fixed) » Active

I tried mr.andrey tutorial, but it throw this error: Fatal error: Call to a member function clone_view() on a non-object in C:\wamp\www\web\sites\all\modules\helper\helper.module on line 17

On line 17 is:

    $temp_view = $view->clone_view(); // create a temp view

Any other options to do that? Tried views_taxonomy_selective_filter-6.x-2.x-dev and hierarchical_select-6.x-3.5 too with no luck :(

dawehner’s picture

Status: Active » Closed (fixed)

Are you kidding us? This is the helper modue. Please don't open issues like this

eL’s picture

Version: 6.x-2.11 » 6.x-2.5

Why? I am asking for support how to solve basical function, which Views are not able to make – Limit a taxonomy dropdown exposed filter to the terms used by nodes in view. Mr.andrey gave a solution with own help module and it did not work for me.

dawehner’s picture

But what on earth has your comment to do with the issue?

eL’s picture

What about, that it is still not solved properly? I dont know, nevermind...

merlinofchaos’s picture

It is not a feature supported by Views. Just because Views does not support a feature you want does not entitle you to hold discussions on it here. The Views issue queue is very very busy, and we do what we can to keep ti as clean as possible (and if you look at the queue stats, you'll see that it's simply not very clean). Moving activity that should not be in the Views queue elsewhere is important for the sanity of those of us who spend a lot of time working in the queue.

I realize that it's a feature you want, but that does not change that the people who work this queue have limited resources.

fossie’s picture

If some has trouble with the jquery part:

    // manually hide the submit button
    // changed this and hide it with jquery, if jquery is not supported, you still see the submit button
//    $form['submit']['#attributes'] = array('style' => 'display:none');

    // submit form on selection change - avoids empty results
    drupal_add_js("$(document).ready(function(){
        $('#".$form['submit']['#id']."').hide();
        $('#".$form['#id']." select').change(function() {
          $('#".$form['#id']."').submit();
        });
    });", 'inline');

I've changed two parts,
- I didn't let drupal disable the submit button and do it in the document ready function
- changed the actual submit, because the form should be submitted and not the button (it wasn't working for me)

If I made a mistake, you can let me know.

HTH,
Fossie

eL’s picture

Try Views hacks module. It has features, that we expected in Views, but that are not here. I.E. limit a taxonomy dropdown exposed filter to the terms used by nodes in view results:

http://drupal.org/project/views_hacks

david_urban’s picture

Version: 6.x-2.5 » 7.x-3.6

Hey mr.andrey, could you or anyone update the code for D7 since db_result is not used anymore? Much appreciated!

Nicolas Bouteille’s picture

If I am right Views Hacks will not work at the beginning if your behavior is not to display any results until the user clicks apply at least once.
If you face this problem here is an article where I explain how to:
- populate Views exposed filters' values with dynamic values. For example, values coming from another view
- make an exposed filter update its values 'on change' of another filter (based on the selected value) using AJAX (i.e. no page reload).

http://bouteillenicolas.com/expertise-drupal/views-ajax-dynamic-dependen...

PetarB’s picture

View Hacks worked nicely for me on this. Highly Recommended.
I must admit, I did think this functionality was part of the core, until I found out it wasn't....
I understand this issue is from 2009, but using Drupal.org's search from finding this solution keeps bringing me to this, so hopefully this helps others.

hugronaphor’s picture

How to be in the case when I have in results, more than 1.500 nodes, the site is simply down, when I call views functions (views_get_current_view, views_get_view, ..).

espurnes’s picture

I modified th #9 code to work with D7 and my custom view. It's a node view with content with taxonomy terms width hierarchy.

I need more test but it's working fine right now.
I saw that the exposed filter has the following structure:

Array
(
    [All] => - Any -
    [0] => stdClass Object
        (
            [option] => Array
                (
                    [38] => footerm1
                )

        )

    [1] => stdClass Object
        (
            [option] => Array
                (
                    [41] => footerm2
                )

        )

    [2] => stdClass Object
        (
            [option] => Array
                (
                    [36] => footerm3
                )

        )

Where $key is the array's key of $form[$filter]['#options'], $value is the value of the array ("-Any-" or an object with other array) and $first_key is the first value of the array inside the object.

Array
(
    [All] => - Any -
    [$key] => stdClass Object
        (
            [option] => Array
                (
                    [$first_key] => footerm1
                )

        )
)

So, I changed the IF condition to check if the tid is in the $used_tids.
The exposed filter is an array of objects containing another array with the term tid and the term name

function hook_form_views_exposed_form_alter(&$form, &$form_state, $form_id){
  
  //checks the form id
  if($form['#id'] == 'views-exposed-form-hs-node-page'){
  	  	  
    // prevent recursion - only execute once
    static $called = 0;
    if ($called === $form['#id']) {
      return;
    }
    
    $called = $form['#id']; // flag as called
    $affected_exposed_filters = array('field_city_zone_tid'); // SET YOUR FILTER NAMES HERE
	
	// get results
    $view = views_get_current_view();
    $view->init_display();
    $view->execute();
    // assemble results into an array
    foreach($view->result as $row) {
      $nids[] = $row->nid;
    }
    
    if(isset($nids)) {
    
		//query to get the used tid in view's results    
		$table_name = 'field_data_field_city_zone'; // Table name
		$table_field_name = 'field_city_zone_tid';	// Table field name
	
		$used_tids_query = db_select($table_name, 'tid')
					->fields('tid',array($table_field_name))
					->condition('entity_id',$nids,'IN')
					->execute();    
			
		foreach($used_tids_query as $result)
		{
			$current_tid = $result->$table_field_name;

			$used_tids[] = $current_tid;

		}			

	
		// unset the unused term options
		foreach($affected_exposed_filters as $filter) {
		/*print '<pre>';
		print_r($form[$filter]['#options']);
		print '</pre>';*/
		  foreach($form[$filter]['#options'] as $key => $value) {

			//if ($key != 'All' ) {
			
				if (isset($value->option)){
			
					reset($value->option); // sets the pointer to first array element
					$first_key = key($value->option); // gets the key of the first array value
														 
					if (!in_array($first_key,$used_tids)) {
						unset($form[$filter]['#options'][$key]);
					}
				}	
		 
		 
			//}
		  }
	  
		}
		
	} // END if(isset($nids))
	else{ drupal_set_message('No results with this filter values. Change the filter values');}
    
  } //END if($form['#id']
}

I hope it helps to someone and if anyone has a better approach is welcome to post it.

espurnes’s picture

Last code was working for an exposed filter based in a term_reference field. If we use an entity_reference field that is related to a taxonomy vocabulary the code is less complex.

// $Id$

function hook_form_views_exposed_form_alter(&$form, &$form_state){
   
  //if we are in the proper view
  if($form['#id'] == 'views-exposed-form-properties-i18n-page-1'){

  	
	// prevent recursion - only execute once
    static $called = 0;
    if ($called === $form['#id']) {
      return;
    }
    
    $called = $form['#id']; // flag as called
    $affected_exposed_filters = array('td'); // SET YOUR FILTER NAMES HERE
	
	// get results
    $view = views_get_current_view();
    $view->init_display();
    $view->execute();
    // assemble results into an array
    foreach($view->result as $row) {
      $nids[] = $row->nid;   
    }
    
    if(isset($nids)) {
    	
		//query to get the used tid in view's results    
		$table_name = 'field_data_field_city'; // Table name
		$table_field_name = 'field_city_target_id';	// Table field name
	
		$used_tids_query = db_select($table_name, 'tid')
					->fields('tid',array($table_field_name))
					->condition('entity_id',$nids,'IN')
					->execute();    
			
		foreach($used_tids_query as $result)
		{
			$current_tid = $result->$table_field_name;

			$used_tids[] = $current_tid;
		}			

	
		// unset the unused term options
		foreach($affected_exposed_filters as $filter) {

		  foreach($form[$filter]['#options'] as $key => $value) {
			
			if ($key != 'All' ) {
				/* unsets the option if the $key is not in the view's results */					 
				if (!in_array($key,$used_tids)) {
					unset($form[$filter]['#options'][$key]);
				}
				

			}
		  }

		}
		

	} // END if(isset($nids))
	else{ drupal_set_message('No results with this filter values. Change the filter values');}
    
  } //END if($form['#id']
} 


it works perfect when the exposed filter is in the view, but it doesn't take effect in a exposed filter displayed in a block... How can change the values of the exposed filter if it's inside a block?

thanks.

jimboh’s picture

Coincidence or what!!
I just completed code to implement this (slightly differently) for exposed filter in block and discovered the same thing. Works only when not exposed in block. Be sure to repost immediately if you solve it as will I. May save one of us a lot of time. The $form variable is changed but not updating the form in the block.

espurnes’s picture

Hi jimboh,

I didn't figured out how to solve the problem yet, but I think this is not working beacause the code it's runing just once to avoid infinite recursion.

    // prevent recursion - only execute once
    static $called = 0;
    if ($called === $form['#id']) {
      return;
    }
    $called = $form['#id']; // flag as called

I think the exposed filter in the block throws the hook the second time, so the code that changes the options in the filter is not executed.

Did you figured out how to solve it?

jimboh’s picture

@espurnes
Terrific well spotted. You are correct, if you remove the recursion check it runs through twice. The result is then as expected.
I cant see any problems with the recursion so I could just remove the check.
But there does not seem any point in running our code twice...
I changed it to only run my code on the second time through and that works fine too.

   static $called = 0;
   $called++;
   //only do second time through
   if ($called == 2) { 
     -- My Code--
   }

Any issues with that?

espurnes’s picture

Hi jimboh,

is this code working for you? it's no working for me.
Can you post your full code to check what I'm missing?

thank you.

jimboh’s picture

I don't know if this will help. I am using a db_query as it was simpler than using the view. I have omitted the details of my query.

function my_module_form_views_exposed_form_alter(&$form, &$form_state, $form_id){
  //checks the form id
  if($form['#id'] == 'views-exposed-form-products-page-1'){
    // prevent recursion - only execute once on second run through
    static $called = 0;
    $called++;
    //only do second time through
    if ($called == 2) { 
      // get current filter values
      $current_filters =$form_state['view']->exposed_input;
      // if filter values are set - (they wont be if no filter has been changed)
      if ((isset($current_filters['field_manufacturer_tid'])) && (isset($current_filters['field_pattern_tid']))) {
         // if manufacturer filter is changed (not 'All) and pattern filter is still 'All'
        if (($current_filters['field_manufacturer_tid'] != 'All') && ($current_filters['field_pattern_tid'] == 'All')) {
           //restrict available pattern options
           $affected_exposed_filters = array('field_pattern_tid'); 
           $sqlstr = 'my sql query string';
           $result = db_query($sqlstr,array(My array of ':params'));
           $used_tids =$result->fetchCol();
        }
        if (isset($used_tids)) {
          foreach($affected_exposed_filters as $filter) {
            foreach($form[$filter]['#options'] as $key => $value) {
              if ($key != 'All' ) {
                if (!in_array($key,$used_tids)) {
                   unset($form[$filter]['#options'][$key]);
                }
              }
            }
          }
          //    drupal_set_message('<pre>'.print_r($form,true).'</pre>');
        }
      }
    }
  } 
} 
espurnes’s picture

This code is not working if you try to get the $used_tids from the current view with:

    $view = views_get_current_view();
    $view->init_display();
    $view->execute();

I just want the terms related to the results of the current view so, achieve this with ans SQL is more complex than get the view's results...
It's a pitty the code does not work with the exposed filter in a block.

mas0h’s picture

Issue summary: View changes

#60 workds for me, but it eliminated some filters from the dropdown that I'm sure they have results.
Any update on this?

Thanks!

teflo’s picture

https://www.drupal.org/node/463678#comment-8456291 work for me!
But the $view->result with a pager get only the specific number of rows.
How can i ask the complete view results without pager?
tnx

neotohin’s picture

@teflo I faced similar issue but used following solution. My solution also avoids selective filter like behaviour. Now even after filtering, exposed filter's options wont get limited.

<?php
function HOOK_form_views_exposed_form_alter(&$form, $form_state) {

  if ($form['#id'] == 'views-exposed-form-in-the-media-ctools-context-1'
  ) {
  // prevent recursion - only execute once
  static $called = 0;
  if ($called === $form['#id']) {
    return;
  }
  $called = $form['#id']; // flag as called

  $affected_exposed_filters = array('tid'); // SET YOUR FILTER NAMES HERE

  // get results
  $curr_view = views_get_current_view();

  // To avoid overriding current view settings so loading different object
  $view = views_get_view($curr_view->name);
  $view->set_display( $curr_view->current_display );

  $view->exposed_input = array();  // Unset Exposed input array to avoid limiting result
  // Removing Pager Options
  $pager = $view->display_handler->get_option('pager');
  $pager['type'] = 'none';
  $view->display_handler->set_option('pager', $pager);

  $view->init_display();
  $view->execute();

  // return if no results
  if (!$view->result) {
    return;
  }

  // assemble results into a comma-separated nid list
  foreach($view->result as $row) {
    $nids[] = $row->nid;
  }

  if( !is_array($nids) or count( $nids ) == 0 )
    return;

  // get the list of used terms
  $used_tids = db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {taxonomy_index} tn WHERE tn.nid IN (:nids)", array( ':nids' => $nids) )->fetchField() ;

  if ($used_tids) {
    $used_tids = explode(',', $used_tids);
  } else {
    $used_tids = array(); // this shoudln't happen, but just in case...
  }


  // unset the unused term options
  foreach($affected_exposed_filters as $filter) {

    foreach($form[$filter]['#options'] as $key => $item) {

      if( !is_numeric( $key )) continue;

      reset( $item->option );
      $tid = key( $item->option );

      if ( !in_array($tid,$used_tids)) {
        unset($form[$filter]['#options'][ $key ]);
      }
    }
  }

  }
}
?>
aminlatif’s picture

I've made a few adjustments to the code so it can be used in the blocks as well.

<?php
function modulename_form_views_exposed_form_alter(&$form, $form_state)
{
    if($form_state['view']->description=='[level2]') {
        return;
    }

    $exposed_filters = array();
    $infos = $form['#info'];
    foreach($infos as $key=>$info) {
        $exposed_filters[] = $info['value'];
    }

    $view = views_get_view($form_state['view']->name);
    $view->set_display($form_state['view']->current_display);
    $view->description = "[level2]";
    $pager = $view->display_handler->get_option('pager');
    $pager['type'] = 'none';
    $view->display_handler->set_option('pager', $pager);
    $view->init_display();
    $view->execute();
    if (!$view->result) {
        return;
    }

    $nids = array();
    foreach($view->result as $row) {
        $nids[] = $row->nid;
    }

    $used_tids = db_query("SELECT GROUP_CONCAT(DISTINCT CAST(tn.tid AS CHAR) SEPARATOR ',') FROM {taxonomy_index} tn WHERE tn.nid IN (:nids)", array( ':nids' => $nids) )->fetchField() ;

    if ($used_tids) {
        $used_tids = explode(',', $used_tids);
    } else {
        $used_tids = array(); // this shoudln't happen, but just in case...
    }

    foreach($exposed_filters as $filter) {
        if (strpos($filter, '_true') == false) {
            continue;
        }
        foreach($form[$filter]['#options'] as $key => $item) {
            if( !is_numeric( $key )) continue;
            reset( $item->option );
            $tid = key( $item->option );
            if (!in_array($tid,$used_tids)) {
                unset($form[$filter]['#options'][ $key ]);
            }
        }
    }
}
?>
Zarkas’s picture

This is wonderful, how can I implement this code?

meladawy’s picture

@aminlatif
More edit to your code...You need to add set_exposed_filter to view query
replace

$view = views_get_view($form_state['view']->name);
    $view->set_display($form_state['view']->current_display);

with

$view = views_get_view($form_state['view']->name);
    $view->set_display($form_state['view']->current_display);
    $view->set_exposed_input(array('field_year_tid ' => 'all')); // replace this with exposed filter field name

This will resolve the issue of losing select options after selecting one value.

Lord Pachelbel’s picture

Views Selective Filters will do this (currently 7.x only). Using that module I was able to make a taxonomy-based filter that only listed tags that were actually being used, and it was really quick.

drupallogic’s picture

subscribing.

Views Selective Filters seems nice though I need to wait for Drupal 8 version.

...and I believe this feature is what Views should be doing allready.

adamcadot’s picture

We must coordinate efforts with core's handling of Views Exposed Filters for Entity Reference fields.

adamcadot’s picture

Version: 7.x-3.6 » 8.x-3.x-dev
DamienMcKenna’s picture

Version: 8.x-3.x-dev » 7.x-3.x-dev

This issue is closed, lets not change its metadata.