AlertThis post is over a year old, some of this information may be out of date.
Part 4: Create a Dropdown Search Refiner Control
In this blog post part of the search refiner control series, I’ll show how to create a dropdown search refiner control. To make it a bit special, I’ve added the functionality of showing the filters that were available before the results were refined.
Creating a dropdown menu for your Search Refiner Control is really simple. The first thing to do is creating the select elements in html, to do this the mark-up of the control needs some modification. For this refiner template I’m going to work with only one dropdown element instead of two (unselected and selected array).
<selectid='ms-ref-unselSec'style='display:_#= $htmlEncode(displayStyle) =#_'onchange="javascript:new Function(this.value)();"><option></option><!--#_
for (var i = 0; i < unselectedFilters.length; i++) {
var filter = unselectedFilters[i];
if(!$isNull(filter)) {
var refiners = new Object();
refiners[filter.RefinerName] = filter.RefinementTokens;
ShowRefiner(filter.RefinementName, filter.RefinementCount, refiners, 'addRefinementFiltersJSON');
}
}
var currentRefinementCategory = ctx.ClientControl.getCurrentRefinementCategory(ctx.RefinementControl.propertyName);
// Check if the object is null or undefined && Count the tokens currently in place
var hasAnyFiltertokens = (!Srch.U.n(currentRefinementCategory) && currentRefinementCategory.get_tokenCount() > 0);
if (selectedFilters.length > 0 '' hasAnyFiltertokens) {
for (var i = 0; i < selectedFilters.length; i++) {
var filter = selectedFilters[i];
if(!$isNull(filter)) {
var refiners = new Object();
refiners[filter.RefinerName] = filter.RefinementTokens;
ShowRefiner(filter.RefinementName, filter.RefinementCount, refiners, 'removeRefinementFiltersJSON');
}
}
}
_#--></select><!--#_
if (selectedFilters.length > 0 '' hasAnyFiltertokens) {
var refinerRemoval = new Object();
refinerRemoval[ctx.RefinementControl.propertyName] = null;
ShowRefiner('Remove refinement', null, refinerRemoval, 'updateRefinersJSON');
}
_#--></div><!-- CONTAINER CLOSING TAG -->
As you can see, I changed the previous DIV elements to select elements. Another thing I’ve done is, I moved the Remove refinement link out of the select element.
The next step is to change is the ShowRefiner function. In that function the hyperlinks need to be changed to option elements.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
functionShowRefiner(refinementName,refinementCount,refiners,method){// Create the onClick or onChange event
varonClickOrChange="$getClientControl(this)."+method+"('"+$scriptEncode(Sys.Serialization.JavaScriptSerializer.serialize(refiners))+"');";// Check if the refinement contains results
if(refinementCount!=null){_#--><optionhref='javascript:{}'value='_#= onClickOrChange =#_'>_#=$htmlEncode(refinementName)=#_</option><!--#_}else{_#--><div><ahref='javascript:{}'onclick='_#= onClickOrChange =#_'>_#=$htmlEncode(refinementName)=#_</a></div><!--#_}}
This results in the following output:
As you can see, creating a custom dropdown refiner control isn’t that hard once you know what need to be updated. Now we go a step further, showing the elements that were there before the refining.
Showing the Unselected Refiners
Once you refined your results, the unselected refiners array will be empty. This is due to the fact that the selected array will be populated once the result set is refined. The selected array contains the possible refiners once refined.
It isn’t possible to retrieve the old (unselected) refinement values from the current ListData object, because it doesn’t contain these values anymore. It now has the refiner values for the new / refined set of results.
The explained approach in that post is to store the results in a container outside the render area of the current display template. If they are stored inside the render area of that display template control, they’ll be removed once the control is refreshes (happens each time you refine).
Note: the following piece of code can be written with jQuery in a “cleaner” and quicker way.
The first thing you’ll need is a hidden container that is used to temporally store the refiner option.
1
2
3
4
5
6
7
8
9
10
11
// Create a new hidden block outside the current refinement control
varrefElm=document.getElementById('Refinement');varhiddenBlockID=ctx.RefinementControl.containerId+"_"+ctx.RefinementControl.propertyName;varhiddenBlock=document.getElementById(hiddenBlockID);// Check if the hidden block exists, otherwise we create one
if(hiddenBlock===null''hiddenBlock.lenght<=0){hiddenBlock=document.createElement('div');refElm.appendChild(hiddenBlock);hiddenBlock.setAttribute('id',hiddenBlockID);hiddenBlock.setAttribute('style','display:none;');}
With this code a new block gets created in the refinement panel right after the search refiner control blocks, I gave it a unique ID to easily retrieve it.
The next step is to change the ShowRefiner function to populate the hidden block with the refiners. This is only needed for the unselected list of refiners, so we can add a check to see if the results aren’t refined.
1
2
3
4
5
6
7
8
9
// Check if there aren't filter tokens in place
if(!hasAnyFiltertokens){varelm=document.getElementById(hiddenBlockID);varoption=document.createElement('option');vartext=document.createTextNode(refinementName);option.appendChild(text);option.setAttribute('value',onClickOrChange);elm.appendChild(option);}
If you test this now, you won’t see anything visual, but if you check the hidden container, you’ll see that it gets populated with the refiners.
This list needs some clean up every time the template starts populating the unselected array. If you wouldn’t do it, you’ll end up with double items. To achieve this, I created a ClearHiddenList function that will be called each time before the unselected loop starts the enumerations.
As I said, the function call will be done just before the unselected array loop. I also added a check to see if unselected array contains items, otherwise the hidden block would be erased after every refresh.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--#_if(unselectedFilters.length>0){// Clear the hidden list
ClearHiddenList();for(vari=0;i<unselectedFilters.length;i++){varfilter=unselectedFilters[i];if(!$isNull(filter)){varrefiners=newObject();refiners[filter.RefinerName]=filter.RefinementTokens;ShowRefiner(filter.RefinementName,filter.RefinementCount,refiners,'addRefinementFiltersJSON');}}}_#-->
Almost there, we just need to append some extra bocks / containers to append the hidden refiners back to the dropdown. For this I’ll use two option grouping optgroup blocks, these blocks get their own IDs.
1
2
3
// Dropdown Group IDs
varunselDD=ctx.RefinementControl.containerId+"_Unsel";varselDD=ctx.RefinementControl.containerId+"_Sel";
The mark-up of these optgroup blocks look like this:
<selectid='ms-ref-unselSec'style='display:_#= $htmlEncode(displayStyle) =#_'onchange="javascript:new Function(this.value)();"><option></option><!--#_
if (selectedFilters.length > 0 '' hasAnyFiltertokens) {
_#--><optgrouplabel="Selected Refiners"id="_#= selDD =#_"><!--#_
for (var i = 0; i < selectedFilters.length; i++) {
var filter = selectedFilters[i];
if(!$isNull(filter)) {
var refiners = new Object();
refiners[filter.RefinerName] = filter.RefinementTokens;
ShowRefiner(filter.RefinementName, filter.RefinementCount, refiners, 'removeRefinementFiltersJSON');
}
}
_#--></optgroup><!--#_
}
_#--><optgrouplabel="Other Refinements"id="_#= unselDD =#_"><!--#_
if (unselectedFilters.length > 0) {
for (var i = 0; i < unselectedFilters.length; i++) {
var filter = unselectedFilters[i];
if(!$isNull(filter)) {
var refiners = new Object();
refiners[filter.RefinerName] = filter.RefinementTokens;
ShowRefiner(filter.RefinementName, filter.RefinementCount, refiners, 'addRefinementFiltersJSON');
}
}
}
_#--></optgroup></select>
Note: for the visual part, I changed the order form the loops. I’ve placed the selected loop above the unselected one.
Right now the result looks like this once you refine your results:
Adding the Hidden Refiners to the Dropdown
To append the hidden refiners to the dropdown, we need to implement a callback function that populate the hidden refiners once the refiner controlled finished rendering.
This can be achieved by using the AddPostRenderCallback function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Run this after the control is rendered - this will populate the unselected option group
AddPostRenderCallback(ctx,function(){if(hasAnyFiltertokens){// Get the hidden block
varhiddenOptions=document.getElementById(hiddenBlockID).children;varunSelGroup=document.getElementById(unselDD);varselGroup=document.getElementById(selDD);// Clone all the elements from the hidden list to the unselected option group
for(vari=0;i<hiddenOptions.length;i++){varselectedElm=GetAllElementsWithAttribute(selGroup,'value',hiddenOptions[i].getAttribute('value').replace('updateRefinersJSON','removeRefinementFiltersJSON'));if(selectedElm===null''selectedElm.length<=0){varcloneElm=hiddenOptions[i].cloneNode(true);unSelGroup.appendChild(cloneElm);}}}});
If you now do a search and refine the results, you will see that the hidden refiners are added to the dropdown.
If you want to use one of the other refiners, you’ll need to change the method that is used for refining the results. We cannot use the addRefinementFiltersJSON, we should use the updateRefinersJSON method instead. This is because the refinement that is in place needs to be updated, instead of adding an extra refinement.
The ShowRefiner function call in the unselected loop should be changed to this:
One last thing is to don’t populate the selected option to the unselected list. To check this we need to add a check in the AddPostRenderCallback function to check if the element is in the selected list.
functionGetAllElementsWithAttribute(element,attribute,value){varmatchingElements=[];varallElements=element.getElementsByTagName('*');for(vari=0;i<allElements.length;i++){if(allElements[i].getAttribute(attribute)){if(value===allElements[i].getAttribute(attribute)){matchingElements.push(allElements[i]);}}}returnmatchingElements;}// Run this after the control is rendered - this will populate the unselected option group
AddPostRenderCallback(ctx,function(){if(hasAnyFiltertokens){// Get the hidden block
varhiddenOptions=document.getElementById(hiddenBlockID).children;varunSelGroup=document.getElementById(unselDD);varselGroup=document.getElementById(selDD);// Clone all the elements from the hidden list to the unselected option group
for(vari=0;i<hiddenOptions.length;i++){varselectedElm=GetAllElementsWithAttribute(selGroup,'value',hiddenOptions[i].getAttribute('value').replace('updateRefinersJSON','removeRefinementFiltersJSON'));if(selectedElm===null''selectedElm.length<=0){varcloneElm=hiddenOptions[i].cloneNode(true);unSelGroup.appendChild(cloneElm);}}}});
The outcome looks as follows:
Set the Selected Item
One last thing that needs to be done, is to set the selected item in the dropdown. This can be achieved by adding a Boolean value to the ShowRefiner function call, so that this value can be used to create a selected option once this value is true. The updated ShowRefiner function looks like this:
functionShowRefiner(refinementName,refinementCount,refiners,method,selected){// Create the onClick or onChange event
varonClickOrChange="$getClientControl(document.getElementById('"+ctx.RefinementControl.containerId+"'))."+method+"('"+$scriptEncode(Sys.Serialization.JavaScriptSerializer.serialize(refiners))+"');";// Check if there aren't filter tokens in place
if(!hasAnyFiltertokens){varelm=document.getElementById(hiddenBlockID);varoption=document.createElement('option');vartext=document.createTextNode(refinementName);option.appendChild(text);option.setAttribute('value',onClickOrChange);elm.appendChild(option);}// Check if the refinement contains results && if the current item is selected
if(refinementCount!=null&&selected!==true){_#--><optionvalue='_#= onClickOrChange=#_'>_#=$htmlEncode(refinementName)=#_</option><!--#_}elseif(refinementCount!=null&&selected===true){_#--><optionvalue='_#= onClickOrChange=#_'selected='selected'>_#=$htmlEncode(refinementName)=#_</option><!--#_}else{_#--><div><ahref='javascript:{}'onclick='_#= onClickOrChange=#_'>_#=$htmlEncode(refinementName)=#_</a></div><!--#_}}
The function call in the selected loop needs to be updated to set the value to true, and the two other calls (unselected loop, and removal link) need to be set to false.
In the next part of this series I’ll explain the methods that can be used for refining your results. Currently we have used a few of them, but I didn’t explain how they work and what they do. These things will be tackled in the part 5.
Changes
03/02/2014
Andy pointed out that the onclick event doesn’t work on dropdown options in Google Chrome. I have modified the code to now support Google Chrome. For this I changed all the onclick attributes to value attributes, and set an onchange attribute on the select element.