[O365] Autocomplete on taxonomy refiners
For a customer project I’m on we’re making use of search to surface pages stored in SharePoint Online. On a predefined search results page, refiners make it easier for users to get to the page they are searching for. Refiners are linked to managed search properties and those are linked to crawled properties. In this case, the field crawled is a managed metadata taxonomy field and the values of that field come from a metadata termset. The problem is that this termset is pretty big because it contains all of the countries worldwide. This is what the refiner looks like:
So what if a user is searching for ‘Switzerland’? Hmmm. Well he or she could click ‘Other Value’ and type in what they are looking for. The problem here is that the value needs to exactly match the term and your user might not have a clue about the values in the termset. Countries is doable, but other labels might be harder to guess.
See how I typed in ‘switzer’ and SharePoint is now refining on that value. Items tagged with ‘Switzerland’ do not appear this way, our end-user is left in doubt. So wouldn’t it be nice to have autocomplete on the textbox so the user is guided in their search? That’s exactly what we proposed to our customer and what I will detail in this blog post.
Creating custom display templates
To implement this, we’ll need to use filter display templates for use with the search refiners. A pretty good tutorial on how to create those by Elio Struyf is found here, so I’m not going to go into detail on that part.
To start off, I made a copy of the Filter_MultiValue and Filter_MultiValue_Body templates. You could also pick other templates to start off with, it doesn’t really matter for the solution we’re building.
Next I changed the body template. By default it renders a configurable number of terms as seen above, but that’s not that useful in this case. So instead of doing that, I changed the template to render only the selected items, which are none by default. Why we want to render the selected ones you’ll see below. To do so, change the following lines (60 – 72):
<!--#_ if(isSelected) { _#--> <input type="checkbox" class="ms-padding0 ms-margin0 ms-verticalAlignMiddle" id="_#= $htmlEncode(inputId) =#_" name="_#= $htmlEncode(inputName) =#_" data-displayValue="_#= $htmlEncode(filter.RefinementName) =#_" value="_#= $htmlEncode(filter.RefinementToken) =#_" checked="" /> <!--#_ } else { _#--> <input type="checkbox" class="ms-padding0 ms-margin0 ms-verticalAlignMiddle" id="_#= $htmlEncode(inputId) =#_" name="_#= $htmlEncode(inputName) =#_" data-displayValue="_#= $htmlEncode(filter.RefinementName) =#_" value="_#= $htmlEncode(filter.RefinementToken) =#_" /> <!--#_ } _#--> <label for="_#= $htmlEncode(inputId) =#_" class='_#= nameClass =#_'> _#= $htmlEncode(filter.RefinementName) =#_
To:
<!--#_ if(isSelected) { _#--> <div id="Value"> <input type="checkbox" class="ms-padding0 ms-margin0 ms-verticalAlignMiddle" id="_#= $htmlEncode(inputId) =#_" name="_#= $htmlEncode(inputName) =#_" data-displayValue="_#= $htmlEncode(filter.RefinementName) =#_" value="_#= $htmlEncode(filter.RefinementToken) =#_" checked="" /> <label for="_#= $htmlEncode(inputId) =#_" class='_#= nameClass =#_'> _#= $htmlEncode(filter.RefinementName) =#_ </label> </div> <!--#_ } _#-->
By default, the template doesn’t show a textbox and you need to click ‘Other Value’ to have it appear. We don’t want that, so we’re just going to render the textbox straight away. Change lines 87 – 94 to:
<div id="OtherValue" class="ui-widget"> Type (part of) the _#= displayTitle =#_. Autocomplete will help you find the right value. <input type="text" name="_#= $scriptEncode(propertyName) =#__txtGroup" class="ms-ref-refineritem"> </div>
As you can see, I added some guiding text for the user. Note that the name of the input textbox will be set to ‘RefinableString1_txtGroup‘, depending on the refiner you’ve configured this template for. This is exactly how the out of the box functionality would render it and the name is important because we need it later on.
So now our refiner renders like this:
Hooking-up jQuery UI autocomplete
Ok cool, but how about that autocomplete? Well here we will need some jQuery and jQuery UI magic. jQuery UI features an autocomplete functionality which is perfect. I first made some changes to my Custom_Filter_MultiValue template. In the <script> tag, load your jquery ui file:
<script> $includeScript("", "~sitecollection/_catalogs/masterpage/Display Templates/Filters/Custom_Filter_MultiValue_Body.js"); $includeScript("", "~sitecollection/Scripts/jquery-ui.min.js"); </script>
And include the following javascript in the template to load some javascript files in the right sequence.
RegisterSod('sp.runtime.js', Srch.U.replaceUrlTokens("~sitecollection/_layouts/15/sp.runtime.js")); RegisterSod('sp.js', Srch.U.replaceUrlTokens("~sitecollection/_layouts/15/sp.js")); RegisterSod('sp.taxonomy.js', Srch.U.replaceUrlTokens("~sitecollection/_layouts/15/sp.taxonomy.js")); RegisterSod('CustomActions.js', Srch.U.replaceUrlTokens("~sitecollection/Scripts/CustomActions.js")); RegisterSodDep('sp.js', 'sp.runtime.js'); RegisterSodDep('sp.taxonomy.js', 'sp.js'); RegisterSodDep('CustomActions.js', 'sp.taxonomy.js'); EnsureScriptFunc('CustomActions.js', null, function () { Custom.Jsom.Refiners.init(); });
This script registers the required js files and their dependencies. Another great post by Elio has more info about that, read it here. Warning: I saw weird things happening as soon as I published the search results page I was working on. The scripts stopped working and that was due to the javascript files not loading in the right sequence. Apparently loading differs in edit mode or something, don’t know. Just make sure you’ve got the above dependencies right and it should work as expected.
Now the real magic is found in the CustomActions.js file:
Custom.Jsom.Refiners = function () { function loadTerms(text, termSetName) { var deferred = $.Deferred(); var locale = 1033; var clientContext = SP.ClientContext.get_current(); var taxonomySession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext); var termStore = taxonomySession.getDefaultSiteCollectionTermStore(); var termSets = termStore.getTermSetsByName(termSetName, locale); var termSet = termSets.getByName(termSetName); var terms = termSet.getAllTerms(); clientContext.load(taxonomySession); clientContext.load(termStore); clientContext.load(termSet); clientContext.load(terms); clientContext.executeQueryAsync( Function.createDelegate(this, function () { deferred.resolve(terms); }), Function.createDelegate(this, function (sender, args) { deferred.reject(sender, args); }) ); return deferred.promise(); } function findTerms(terms, text) { var termEnumerator = terms.getEnumerator(); var termList = []; text = text.toLowerCase(); while(termEnumerator.moveNext() && termList.length < 10) { var currentTerm = termEnumerator.get_current(); var termLabel = currentTerm.get_name().toLowerCase(); // if the termlabel starts with the search string; add to results list if (termLabel.indexOf(text) === 0) { termList.push(termLabel); } } return termList; } var init = function() { $("input[name^='RefinableString1'").autocomplete({ source: function (request, response) { loadTerms(request, "Countries").done(function (terms) { response(findTerms(terms, request.term)); }); } }); // insert css tag to load jquery.min.ui var siteUrl = _spPageContextInfo.siteAbsoluteUrl; var link1 = document.createElement('link'); link1.rel = 'stylesheet'; link1.type = 'text/css'; link1.href = siteUrl + "/Style%20Library/jquery-ui.min.css"; document.getElementsByTagName('head')[0].appendChild(link1); } return { init: init }; }();
There’s a couple of things going on here. The init method takes care of attaching the autocomplete to the textbox. Note that the name of the control is fixed (“RefinableString1”), as is the name of the termset (“Countries”). This is because there is no direct link between the refinable string and the termset it’s values come from. Remember managed properties vs crawled properties? A refiner can be mapped to virtually any property in SharePoint, so you need to specify which termset to use somewhere, I chose this approach but agree that there might be better options out there to do this.
Another thing the init method does is loading the jquery-ui CSS file. This is required to niceely style the autocomplete box. You can load CSS files in other ways, I like this because it’s clean, simple and only loads when needed.
The loadTerms function takes care of connecting to the sites termstore and querying the terms in the termset. It uses jQuery promises to wait for the load to finish.
The findTerms function takes the set of terms and does a simple compare of the label with the string entered by our end-user. Yes, this can be optimized too cause you probably want the term query to do this and not get all of the terms over the line. Can be done, but not too relevant for the example and I didn’t bother to at this time. Performance is quite ok so I think this is not really required unless you have really huge termsets.
Finally, this array of term labels is passed to the response function of jQuery’s autocomplete. And voila:
We can now select switzerland from our autocomplete list and apply the value to the refiner!
Notice how the “switzerland” selected value now appears? That’s because the template is only rendering the selected items. In my opinion this is more intuitive because it gives the user feedback on what they selected (and why not all search results are shown). You might choose to remove the checkbox though, up to you.
Conclusion
Pretty neat to have autocomplete, yes? Do consider the following though. The autocomplete box will now use all terms in the termset. There might not be any search results for all terms depending on the content, so some refiners might end up returning 0 results. This is different then default SharePoint behaviour where only refiners are shown for which there are actual results. I might investigate in the future on how to mimic this. Instead of getting all the terms, we could get all of the refiner values for a specific refiner. This would also enable us to remove the link between a refiner name and the termset name, which is kinda ugly. Ah well, for now this will do and I hope you enjoy this little bit of code 🙂
I’m sorry but due to this being a customer project I cannot share the actual sourcecode. If lots of you want to have the sources, let me know below and I’ll create a sample project for it.
September 21, 2015 at 9:55 pm |
[…] my previous O365 post, I showed how you can make use of jQuery and jQuery UI to make a search refiner filter template […]
February 10, 2017 at 12:48 pm |
Hi Jasper, could you share above sourcecode in a sample project?
February 20, 2017 at 6:31 pm |
Hi Robert,
I’m afraid I can’t. The code is part of a customer project which I cannot share and apart from that I’m no longer on that project so I can’t access it anymore either. Sorry but I don’t have the time to build you a sample solution at the moment.
August 27, 2017 at 10:40 am |
Hi, wonderfull starter article but I think this won’t work as asumed. I did the changes as you mentioned above, but it does not filter me the right (or all results) out. Because your code create a URL Query Syntax like this
http://superserver/sysb/Seiten/56654654.aspx#Default={“k”:””,”r”:[{“n”:”mycustomTaxRefiner”,”t”:[“equals(\”Betrieb\”)”],”o”:”OR”,”k”:false,”m”:{“equals(\”Betrieb\”)”:”Betrieb”}}]}
This url does a filter on the text, so I think. But the search result will be empty, I tried this with an existing term, it does not result me anything. So I tried to compare the urls with the generated url, when I click a refiner value in the refiner it self. This generates the following url
http://superserver/sysb/Seiten/56654654.aspx#Default={“k”:””,”r”:[{“n”:”mycustomTaxRefiner”,”t”:[“string(\”L0|#087faa432-0001-0008-0000-e04330b12acd|Betrieb\”)”],”o”:”OR”,”k”:false,”m”:{“string(\”L0|#087faa432-0001-0008-0000-e04330b12acd|Betrieb\”)”:”Betrieb”}}]}
This works to when you set only “Betrieb” as values. The main difference is when you set the “string” part instead of “equals”. Did you changed your code afterwards a little bit?
August 28, 2017 at 7:17 am |
I’m pretty sure I didn’t change it afterwards. But it could be that behavior in SharePoint has changed in between versions or updates of course. Anyways, thanks for posting this additional info, very helpful!