Views AJAX Dynamic Dependent Exposed Filters
What will this article tell you ?
- 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).
NB: please note that the AJAX part of the following solution only works for single-select exposed filters for now, not multiple-selects. However, for clarity purpose, I'll be using multiple-selects in some of my screenshots. EDIT : looks like it is possible, see comments below if you need it, I haven't had the chance to try it yet, will update the article when I can.
A concrete example of what we want to do
1) Populate exposed filters with dynamic values
Say you have a search page of Products that you want your visitors to be able to filter by Color and Shape.
You have a lot of possible colors and shapes registered on your website and for some reasons some of them may not return any results yet. For example there could be no 'green' product on your site yet. So if a visitor tried to look for 'green' products he or she would get a no-results-found message.
Because we are great web designers, we want to avoid that. Right ? Available colors on the search page should only be those that could actually bring some results back. In our example: red, black and blue only.
But, we still want to keep unused values registered into our website so that the day a webmaster needs to add a 'green' product the color would actually be there to be checked. Right ? That's why we need to find a way to tell our exposed filter to ONLY display values that interest us. Values that do have products refering to them. Values that will return results.
How do we do this with Views ? That's one of the two things this article is about.
2) Make one exposed filter update its values 'on change' of another filter using AJAX
Say you have some red & round Products and some black & square Products but NO black & round ones... This means that red and black are going to be available as filtering values on the search page, as for round and square. As a consequence, your visitors could actually try to look for black & round products and thus, get a no-results-found message !
Fortunately, thanks to AJAX we can make sure that when 'round' is checked, 'black' disappears from the available colors.
How can this be done ? That's the second part of the article.
Overview of the solution
1) Populate exposed filters with dynamic values coming from another view
So, we want to make sure the color filter only displays colors that would return products. This implies two things:
- being able to list those used-only colors
- being able to tell the exposed filter to use this list for its values
For our list of used-only colors we won't have to hesitate much as Views is THE easiest and most powerful solution we have. So we'll simply have to create a view for that.
Now, regarding the 'telling the exposed filter to use our view' part, this is not part of the UI right now so we'll have to do some custom code there. Nothing fancy though.
2) Make one exposed filter update its values 'on change' of another filter using AJAX
Now, how do we make sure ONLY the colors that are used by at least one Product of the currently selected Shape are displayed ? Gathering the correct colors won't be too hard. We'll just have to modify our view so that it can accept and deal with an input parameter. Basically, we'll just have to add a contextual filter.
The tricky part is updating the available colors 'on change' of the shape filter via AJAX. It would be great if we could just tell an exposed filter to update its values based on another one via AJAX through the UI. Unfortunately, this is not possible yet. Actually this is kind of logical considering that is it not possible to provide dynamic values to an exposed filter yet. Why would it need to refresh itself via AJAX when it cannot even change ?
Fortunately this still can be achiveved through custom coding. Futhermore, Drupal has a great AJAX Forms framework that will make it a real piece of cake !
The solution in detail
1) Populate exposed filters with dynamic values coming from another view
a) Creating the view
First of all, even though colors and shapes are Taxonomy Terms, we want to make sure we configure our View as one showing 'Content' of type 'Product', not 'Taxonomy Terms' of type 'Color'.
Why ? Because we only want to show colors that do have Products refering to them. Thereby, even though we want to display colors, we actually need to gather all products first. Only then can we see which colors are in use.
Basically, instead of displaying regular info about the product such as its title, description, etc... we will only display its color and then we'll just have to tell Views to remove duplicate rows and we'll have our list of used-only colors !
We could simply add the field color of the Product node, but by doing so we wouldn't be able to make sure that each color only appears once (even using the pure disctinct option). Try it and you'll see.
To make sure we only get distinct colors, we need to bring the Color as a Views Relationship and actually display the name of Taxonomy Term, using the Color Relationship.
The Relationship needs to be based on the 'color' Taxonomy Term Reference field of our 'Product' Content Type.
The 'Require this relationship' option should be checked. Indeed, some products may not have any color assigned to them yet if the field is not required. Thus, checking this option will prevent products with no color from being brought as a result row. Remember: we are only looking for colors here...
Once the relationship is created, we can now remove the default 'title of the node' field and replace it with the field 'Taxonomy term: name' that we will be able to bind to our Color relationship.
Now when we check Distinct + Pure Distinct on the query settings, each color is displayed only once.
Great! We now have a list of all the colors that do have at least one product "wearing" them. We're almost done with the view, but there is one last thing that needs to be done. Since we plan on using this list to populate an exposed filter - which can be a select, radio-buttons or checkboxes - we actually need to provide a 'value' for each option. This value has to be the term id for the mechanism to work. This won't be a problem as we'll just need to add a second field to display: the 'Taxonomy Term: Term ID' field, binding it to the color relationship again.
That's it! Our view is now ready to be used to populate our colors exposed filter.
2) Telling the exposed filters to use the view
As I told you earlier, it is not possible to tell our colors exposed filter to use our freshly created view to populate itself through the UI. Fortunately, it is very easy to do it using custom coding. Basically we're gonna alter the exposed filters form using a hook() and because we want to do it right, we're gonna do that in a custom module of our own.
If you don't know how to create a module already, I suggest you shoud learn how to create a module here. Creating a basic custom module is the right way to add custom code because that way you can be sure it won't be erased by a module or core update one day. Since it isn't complicated at all, my advice is read it now :-)
From what I know, the easiest way to alter our exposed filters is to access the exposed form via hook_form_views_exposed_form_alter and imediately checking the id of the form is ours :
<?php
function vaddef_form_views_exposed_form_alter(&$form, &$form_state){
if ($form['#id'] == 'views-exposed-form-products-page') {
dpm($form);
}
}
?>
Remember to clear your Drupal site caches the first time you install your module or add a new function to it. (Configuration > Development > Performances > Clear all the caches ; even easier with the Administration Menu module)
By the way, if you don't know how to get your form's #id simply use a tool like firebug on the form element :
Now that we know we're altering the right form, let's modify the values of the colors exposed filter. For that we're gonna need to know its id too. The Drupal Print Message function dpm($form) provided by the Devel module will allow us to get it. Mine is $form['colors'] and its values are stored in $form['colors']['#options']. Now that we know that, let's try a little test :
<?php
function vaddef_form_views_exposed_form_alter(&$form, &$form_state){
if ($form['#id'] == 'views-exposed-form-products-page') {
$form['colors']['#options'] = array(
'1' => 'Red',
'2' => 'Blue',
'4' => 'Black',
);
}
}
?>
Here I manually restricted the availables colors to red, blue and black. This ain't dynamic yet, but it is a good test to see if things are working as expected before mooving on. Note that 1, 2 and 4 are the term ids of the respective colors I listed. You can see those ids when editing a term, the id is at the end of the url.
Now only those three colors should be available. Provided you gave the correct term ids, the filtering should also still work.
Ok but what we want is to use the values returned by our other view to populate the exposed filter. Well, that's what the following code does:
<?php
function vaddef_form_views_exposed_form_alter(&$form, &$form_state){
if ($form['#id'] == 'views-exposed-form-products-page') {
$form['colors']['#options'] = _get_associative_array_from_view(
'colors_with_products', // view id
'default', // view display id
'taxonomy_term_data_field_data_field_color_tid', // key field id
'taxonomy_term_data_field_data_field_color_name' // value field id
);
}
}
?> <?php
function _get_associative_array_from_view($viewID, $viewDisplayID, $keyFieldID, $valueFieldID){
$associativeArray = array();
$associativeArray['All'] = t('- Any -');
$viewResults = views_get_view_result($viewID, $viewDisplayID);
foreach($viewResults as $viewRow) {
$associativeArray[$viewRow->$keyFieldID] = $viewRow->$valueFieldID;
}
return $associativeArray;
}
?>
Instead of setting the values manually, this time we call a custom function _get_associative_array_from_view that returns the expected associative array. This function uses the views_get_view_result() API function of Views to get an array of results. That's why we need to provide the id of the view and of its display we want to target. The id of the view is the one you gave it when creating it but you can also read it at the end of the url when editing the view. The id of the display is default for the master display and for the other kind of displays (page, block, etc) you can find it in the advanced fieldset under "OTHER".
To build the associative array from the view results, we also need the ids of the two fields that will be used as key and value. To find out what are the ids of the fields you need, just make a dpm() of the $viewResults variable that way you can inspect the structure:
So, I guess this finishes the "how to populate an exposed filter with dynamic values coming from another view" section! Let's head to the AJAX magic updating stuff...
2) Make one exposed filter update its values 'on change' of another filter using AJAX
a) Add a contextual filter to the populating view
For our view to be able to return different values according to one input parameter, we need to add a Contextual Filter. For the list of available colors, let's add a contextual filter based on the 'shape' field. 'When the filter value is not available', the view should 'display all results for the specified field'.
We should also 'specify a validation criteria'. Making sure that the input value is the id of a Taxonomy Term belonging to the Shape vocabulary. If the validation fails, Views should display all results again.
NB: even though I've been using multiple-selects in my screenshots for clarity purpose, as mentionned earlier my solution will only work for single-selects. Indeed, it seems to me that Views can handle only one value by contextual filter. Even if we choose 'Filter value type' to be 'Term IDs separated by , or +' it seems that Views will then only be able to correctly parse the url but will still use only the first token and thus use only one value for the filter. For this reason I haven't been able to apply my solution to multiple-selects that can accept multiple selected values.
Ok, let's try it on. Enter the term id of different shapes in the preview bar. The available colors should filter correctly. In this example, 14 is the id of the "Round" shape and we can see there are only blue and red round shapes.
You can find the id of any taxonomy term such as a shape at the end of the url when editing the term.
b) Add an input parameter to be used as the contextual filter
Before we deal with the AJAX aspect of the solution (updating the values on the fly with no page reload), let's make sure this actually works when the page DOES reload... For example, if "Round" is selected and we click "Apply", available colors on page reload should not be "blue, red and black" anymore but only "blue and red".
That's what the following updated code does:
<?php
function vaddef_form_views_exposed_form_alter(&$form, &$form_state){
if ($form['#id'] == 'views-exposed-form-products-page') {
$selectedShape = $form_state['input']['shapes'];
$form['colors']['#options'] = _get_associative_array_from_view(
'colors_with_products', // view id
'default', // view display id
'taxonomy_term_data_field_data_field_color_tid', // key field id
'taxonomy_term_data_field_data_field_color_name', // value field id
$selectedShape // term id of the selected shape
);
}
}
?> <?php
function _get_associative_array_from_view($viewID, $viewDisplayID, $keyFieldID, $valueFieldID, $contextualFilter){
$associativeArray = array();
$associativeArray['All'] = t('- Any -');
$viewResults = views_get_view_result($viewID, $viewDisplayID, $contextualFilter);
foreach($viewResults as $viewRow) {
$associativeArray[$viewRow->$keyFieldID] = $viewRow->$valueFieldID;
}
return $associativeArray;
}
?>
First of all, we need to get the selected value of the shape exposed filter. In my case using $form_state['input']['shapes']. If you need to inspect your structure, just use dpm() on the $form_state. We then need to provide this selected shape to Views. For that we need to add another parameter to our call to views_get_view_result() and as a consequence also to our custom _get_associative_array_from_view() function. Views will understand this final argument is a contextual filter.
That's it. Try to select as shape and submit your form. When the page reloads available colors should only be those of the products that have the currently selected shape.
c) Update the filter values 'on change' of another filter with AJAX
Ok, we now want the available colors to be updated instantly when choosing a shape without reloading the page. This requires to use the Drupal AJAX Form API. Basically, what the framework is gonna do is recreate ONLY the field colors, 'on change' of the field shape. That's what the following code does:
<?php
$form['shapes']['#ajax'] = array(
'callback' => '_update_colors_callback',
'wrapper' => 'colors_wrapper',
);
$form['colors']['#prefix'] = '<div id="colors_wrapper">';
$form['colors']['#suffix'] = '</div>';
?> <?php
function _update_colors_callback($form, $form_state) {
return $form['colors'];
}
?>
First, we add the #ajax attribute to the shapes field. The ajax will trigger on the default 'change' event of the select and it will replace the given 'wrapper' with the value returned by the given 'callback'. Since we want the colors field to be recreated, we called our wrapper 'colors_wrapper'. The framework will look for a container with the css ID 'colors_wrapper'. This one does not exist yet so we need to set the #prefix and #suffix of the colors field in order to wrap it with our 'colors_wrapper' div. Finally, we need to create our callback function which simply returns ONLY the colors field from the form. Note that when this field is recreated, hook_form_views_exposed_form_alter functions will be called, as expected.
At the time of this writing, this code won't work with Views (currently version 3.5) because Views does not integrate correctly with the Drupal Ajax Form API as explained in this thread: http://drupal.org/node/1183418. Good news however, the patch provided on post #33 does the magic. (There might be even better patches now, or it might even already been commited)
One problem mentioned in this thread remains unsolved by the patch: the fact that Views stores the input filters selected values in the form attribute called 'input' where Drupal Form API stores the submitted values in the attribute 'values'. So when the ajax event is triggered, the selected shape is stored in the 'values' attribute but our code looks inside the 'input' attribute.
The solution I used was one suggested earlier in the thread: merge the to arrays. Which you can do by adding this code at the beginning of your hook_form_views_exposed_form_alter function
<?php
if(!empty($form_state['values'])) {
$form_state['input'] = array_merge($form_state['input'], $form_state['values']);
}
?>
Now it should all be working! Next step is to do the same work for the field shape and make sure it updates dynamically on change of the color filter.
I hope this article is complete, understandable and optimized! Please leave a comment if you have any suggestion regarding how to improve it or if you find any correction that should be done.
Cheers,
Nick.
Commentaires
Bob Rohrer (non vérifié)
jeu, 09/06/2018 - 06:06
Thanks for this writeup as it's a very smart concept and I look forward to getting the right results.
I was trying to do this on a D8 site, but became stuck getting the taxonomy id/name. So I built this verbatum on a D7 site and figured out the taxonomy issue I had been running into. However, I still have not been able to get the Shape selection to limit Colors available, even on the D7 site using your shape/color example.
I assume you are talking about using two views because you wrote that "what we want is to use the values returned by our other view to populate the exposed filter".
The confusion I seem to have is how/which view to apply the exposed filter(s). Am I understanding correctly to use the first exposed filter (for Shapes) with its contextual filter from the first view to limit a 2nd view's available selection of Colors within the 2nd exposed filter? How to connect the exposed filter from a 2nd view to use the list of values from the first view's exposed filter?
Could you please clarify the creation of each view and it's exposed and contextual filters needed for each?
Bob Rohrer (non vérifié)
jeu, 09/13/2018 - 21:34
I figured all this out for D8 ... the D7 concept is the same but there's a bit different access, etc. The problem I was having before was how to pass exposed filter arguments to contextual filters - wasn't sure if needed another view as this article was insinuating another view when stating "values returned by our other view to populate". Simply needed to make sure exposed form in block checked to yes.
Here's a link to the working code I posted for Drupal 8 conversion:
https://drupal.stackexchange.com/a/269296/18513
Paoula Annouza (non vérifié)
ven, 11/30/2018 - 15:00
Thank you for your post, I look for such article along time, today i find it finally. this post give me lots of advise it is very useful for me.
Homes for sales in ajax
Louis (non vérifié)
jeu, 05/02/2019 - 14:19
Thanks for this its just what I need. However I get down to this part.
2) Telling the exposed filters to use the view
You provide a screenshot of what looks like the finished product, the colors and filter by shape. All I have is the list of colors and their IDs. If I don't have a form and therefore no form ID, how can I build the module?
What steps am I missing?
Much appreciated.
Louis (non vérifié)
jeu, 05/02/2019 - 14:44
Just realized this feature is for the content type form. My requirement is a FAQ that has two taxonomy entity references. The final result should be displaying the colors as tabs and display the corresponding shapes. Then according to the selection show the FAQ Question and Answer. So this looks like it would not work in my application. Thanks though!
mostpha (non vérifié)
sam, 02/08/2020 - 16:44
https://www.rjeem.com/
https://www.rjeem.com/praying-istikhaarah-without-prayer/
Thanks for the tutorial, it seems to be the only option I can find online for my project. But, I'm having some issues following the steps. I can't seem to get the right form ID, where you have 'colors', I can't get dpm to print out the correct ID. Is there a possibility you have a more detailed tutorial, step by step that would help us newbies?
Nicolas Bouteille
lun, 02/10/2020 - 15:02
Hi Mostafa,
When you use a hook function such as hook_views_exposed_form_alter, it will be executed on every page that displays a Views Exposed Form.
Make sure you clear your caches and then dpm($form['#id']) will give you the id of the form you're looking for.
Skouf (non vérifié)
mer, 02/26/2020 - 14:29
Hello
I managed to make it work for Drupal 8. But I am stuck on the AJAX part.
Each time I try to change my first select throws me an error :
An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (2G) that this server supports.
What am I missing ?
Here is my code
<?php
function filt_form_views_exposed_form_alter(&$form, $form_state){
//dpm($form);
if ($form['#id'] == 'views-exposed-form-filtres-page-1') {
//kint($form_state);
$selectedTypeRaccordement = $form_state->getValue('field_type_de_raccordement_target_id');
$form['field_composants_target_id']['#options'] = _get_associative_array_from_view(
'filtres_exposes', // view id
'default', // view display id
'taxonomy_term_field_data_node__field_composants_tid', // key field id
'taxonomy_term_field_data_node__field_composants_name', // value field id
$selectedTypeRaccordement // term id of the selected shape
);
$form['field_type_de_raccordement_target_id']['#ajax'] = array(
'callback' => '_update_composants_callback',
'wrapper' => 'composants_wrapper',
);
$form['field_composants_target_id']['#prefix'] = '<div id="composants_wrapper">';
$form['field_composants_target_id']['#suffix'] = '</div>';
}
}
?>
<?php
function _get_associative_array_from_view($viewID, $viewDisplayID, $keyFieldID, $valueFieldID){
$associativeArray = array();
$associativeArray['All'] = t('- Any -');
$viewResults = views_get_view_result($viewID, $viewDisplayID);
//dpm($viewResults);
foreach($viewResults as $viewRow) {
$associativeArray[$viewRow->$keyFieldID] = $viewRow->$valueFieldID;
}
return $associativeArray;
}
?>
<?php
function _update_composants_callback($form, $form_state) {
return $form['field_composants_target_id'];
}
?>
Nicolas Bouteille
mer, 02/26/2020 - 14:40
Sorry pal, I don't have a clue what's going on here. I would advise to find in Drupal's code where/when this specific error message is thrown to better understand what's happening and/or to debug the process with XDebug for example.
Good luck!
Skouf (non vérifié)
mer, 02/26/2020 - 15:25
Thanks for your answer.
I don't know if it will help, but I tried to put this code into hook_form_alter
function filt_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
$form['field_type_de_raccordement_target_id']['#ajax'] = array(
'callback' => '_update_composants_callback',
'wrapper' => 'composants_wrapper'
);
$form['field_composants_target_id']['#prefix'] = '<div id="composants_wrapper">';
$form['field_composants_target_id']['#suffix'] = '</div>';
}
?>
And not putting these lines into hook_form_views_exposed_form_alter
Now I don't have any AJAX errors.
But it's not populating the second select box correctly. So not working. But maybe it will give you an idea.
Nicolas Bouteille
mer, 02/26/2020 - 15:33
Sorry but to be honest I wrote all this in 2012 and I don't have it in mind today anymore :( and this is all Drupal 7 code and since you're trying to make it work on Drupal 8 I'm not sure what can be used or not...
Pages