{"id":1549,"date":"2015-09-14T19:13:34","date_gmt":"2015-09-14T18:13:34","guid":{"rendered":"http:\/\/blog.repsaj.nl\/?p=1549"},"modified":"2015-09-15T07:28:27","modified_gmt":"2015-09-15T06:28:27","slug":"o365-autocomplete-on-taxonomy-refiners","status":"publish","type":"post","link":"http:\/\/blog.repsaj.nl\/index.php\/2015\/09\/o365-autocomplete-on-taxonomy-refiners\/","title":{"rendered":"[O365] Autocomplete on taxonomy refiners"},"content":{"rendered":"<p>For a customer project I&#8217;m on we&#8217;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:<\/p>\n<p><a href=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/taxonomyrefiner.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-1551\" src=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/taxonomyrefiner.png\" alt=\"taxonomyrefiner\" width=\"166\" height=\"513\" srcset=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/taxonomyrefiner.png 166w, http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/taxonomyrefiner-97x300.png 97w\" sizes=\"(max-width: 166px) 100vw, 166px\" \/><\/a><\/p>\n<p>So what if a user is searching for &#8216;Switzerland&#8217;? Hmmm. Well he or she could click &#8216;Other Value&#8217; and type in what they are looking for. The problem\u00a0here is that the value needs to\u00a0<strong>exactly match<\/strong> 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.<\/p>\n<p><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-1561\" src=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/refiner_switzer.png\" alt=\"refiner_switzer\" width=\"157\" height=\"147\" \/><\/p>\n<p>See how I typed in &#8216;switzer&#8217; and SharePoint is now refining on that value. Items tagged with &#8216;Switzerland&#8217; do not appear this way, our end-user is left in doubt. So\u00a0wouldn&#8217;t it be nice to have autocomplete on the textbox so the user is guided in their search? That&#8217;s exactly what we proposed to our customer and what I will detail in this blog post.<\/p>\n<p><!--more--><\/p>\n<h2>Creating custom display templates<\/h2>\n<p>To implement this, we&#8217;ll need to use <strong>filter display templates<\/strong> for use with the search refiners. A pretty good tutorial on how to create those by Elio Struyf is found <a href=\"http:\/\/www.eliostruyf.com\/part-1-create-first-search-refiner-control-template\/\" target=\"_blank\">here<\/a>, so I&#8217;m not going to go into detail on that part.<\/p>\n<p>To start off, I made a copy of the\u00a0<strong>Filter_MultiValue<\/strong> and\u00a0<strong>Filter_MultiValue_Body<\/strong> templates. You could also pick other templates to start off with, it doesn&#8217;t really matter for the solution we&#8217;re building.<\/p>\n<p>Next I changed the body template. By default it renders a configurable number of terms as seen above, but that&#8217;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&#8217;ll see below. To do so, change the following\u00a0lines (60 &#8211; 72):<\/p>\n<pre class=\"prettyprint\">&lt;!--#_\r\n\tif(isSelected) {\r\n_#--&gt;\r\n\t&lt;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=\"\" \/&gt;\r\n&lt;!--#_\r\n\t} else {\r\n_#--&gt;\r\n\t&lt;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) =#_\" \/&gt;\r\n&lt;!--#_\r\n\t}\r\n_#--&gt;\r\n\t&lt;label for=\"_#= $htmlEncode(inputId) =#_\" class='_#= nameClass =#_'&gt;\r\n\t_#= $htmlEncode(filter.RefinementName) =#_\r\n<\/pre>\n<p>To:<\/p>\n<pre class=\"prettyprint\">&lt;!--#_\r\n if(isSelected) {\r\n_#--&gt;\r\n &lt;div id=\"Value\"&gt;\r\n &lt;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=\"\" \/&gt;\r\n &lt;label for=\"_#= $htmlEncode(inputId) =#_\" class='_#= nameClass =#_'&gt;\r\n _#= $htmlEncode(filter.RefinementName) =#_\r\n &lt;\/label&gt;\r\n &lt;\/div&gt;\r\n&lt;!--#_ } _#--&gt;\r\n<\/pre>\n<p>By default, the template doesn&#8217;t show a textbox and you need to click &#8216;Other Value&#8217; to have it appear. We don&#8217;t want that, so we&#8217;re just going to render the textbox straight away. Change lines 87 &#8211; 94 to:<\/p>\n<pre class=\"prettyprint\">&lt;div id=\"OtherValue\" class=\"ui-widget\"&gt;\r\n Type (part of) the _#= displayTitle =#_. Autocomplete will help you find the right value.\r\n &lt;input type=\"text\" name=\"_#= $scriptEncode(propertyName) =#__txtGroup\" class=\"ms-ref-refineritem\"&gt;\r\n&lt;\/div&gt;\r\n<\/pre>\n<p>As you can see, I added some guiding text for the user. Note that the name of the input textbox will be set to &#8216;<strong>RefinableString1_txtGroup<\/strong>&#8216;, depending on the refiner you&#8217;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\u00a0later on.<\/p>\n<p>So now our refiner renders like this:<\/p>\n<p><a href=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/customrefiner.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-1553\" src=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/customrefiner.png\" alt=\"customrefiner\" width=\"185\" height=\"174\" \/><\/a><\/p>\n<p>&nbsp;<\/p>\n<h2>Hooking-up jQuery UI autocomplete<\/h2>\n<p>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 <strong>Custom_<\/strong><strong>Filter_MultiValue <\/strong>template. In the &lt;script&gt; tag, load your jquery ui file:<\/p>\n<pre class=\"prettyprint\">&lt;script&gt;\r\n$includeScript(\"\", \"~sitecollection\/_catalogs\/masterpage\/Display Templates\/Filters\/Custom_Filter_MultiValue_Body.js\");\r\n$includeScript(\"\", \"~sitecollection\/Scripts\/jquery-ui.min.js\");\r\n&lt;\/script&gt;\r\n<\/pre>\n<p>And include the following javascript in the template to load some javascript files in the right sequence.<\/p>\n<pre class=\"prettyprint\">RegisterSod('sp.runtime.js', Srch.U.replaceUrlTokens(\"~sitecollection\/_layouts\/15\/sp.runtime.js\"));\r\nRegisterSod('sp.js', Srch.U.replaceUrlTokens(\"~sitecollection\/_layouts\/15\/sp.js\"));\r\nRegisterSod('sp.taxonomy.js', Srch.U.replaceUrlTokens(\"~sitecollection\/_layouts\/15\/sp.taxonomy.js\"));\r\nRegisterSod('CustomActions.js', Srch.U.replaceUrlTokens(\"~sitecollection\/Scripts\/CustomActions.js\"));\r\n\r\nRegisterSodDep('sp.js', 'sp.runtime.js');\r\nRegisterSodDep('sp.taxonomy.js', 'sp.js');\r\nRegisterSodDep('CustomActions.js', 'sp.taxonomy.js');\r\n\r\nEnsureScriptFunc('CustomActions.js', null, function () {\r\nCustom.Jsom.Refiners.init();\r\n});\r\n<\/pre>\n<p>This script registers the required js files and their dependencies. Another great post by Elio has more info about that, read it <a href=\"http:\/\/www.eliostruyf.com\/correctly-including-scripts-display-templates\/\" target=\"_blank\">here<\/a>.\u00a0<strong>Warning:<\/strong> 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&#8217;t know. Just make sure you&#8217;ve got the above dependencies right and it should work as expected.<\/p>\n<p>Now the real magic is found in the CustomActions.js file:<\/p>\n<pre class=\"prettyprint\">Custom.Jsom.Refiners = function () {\r\n\r\n\tfunction loadTerms(text, termSetName)\r\n\t{\r\n        var deferred = $.Deferred();\r\n\r\n\t\tvar locale = 1033;\r\n\t\tvar clientContext  = SP.ClientContext.get_current();\r\n\t\tvar taxonomySession = SP.Taxonomy.TaxonomySession.getTaxonomySession(clientContext);\r\n\t\tvar termStore = taxonomySession.getDefaultSiteCollectionTermStore();\r\n\t\tvar termSets = termStore.getTermSetsByName(termSetName, locale);\r\n\t\tvar termSet = termSets.getByName(termSetName);\r\n\t\tvar terms = termSet.getAllTerms();\r\n\t    \r\n    \tclientContext.load(taxonomySession);\r\n\t\tclientContext.load(termStore);\r\n\t\tclientContext.load(termSet);\r\n\t\tclientContext.load(terms);\r\n   \r\n        clientContext.executeQueryAsync(\r\n            Function.createDelegate(this, function () { deferred.resolve(terms); }),\r\n\t        Function.createDelegate(this, function (sender, args) { deferred.reject(sender, args); })\r\n\t    );\r\n\t    \r\n\t    return deferred.promise();\r\n\t}\r\n\t\r\n\tfunction findTerms(terms, text)\r\n\t{\r\n    \tvar termEnumerator = terms.getEnumerator();\r\n\t  \tvar termList = [];\r\n\t  \ttext = text.toLowerCase();\r\n\t  \t\r\n\t\twhile(termEnumerator.moveNext() &amp;&amp; termList.length &lt; 10)\r\n\t\t{\r\n\t\t\tvar currentTerm = termEnumerator.get_current();\r\n\t\t\tvar termLabel = currentTerm.get_name().toLowerCase();\r\n\t\t\t\r\n\t\t\t\/\/ if the termlabel starts with the search string; add to results list\r\n\t\t\tif (termLabel.indexOf(text) === 0)\r\n\t\t\t{\r\n\t\t\t\ttermList.push(termLabel);\r\n\t\t\t}\r\n\t\t}\r\n\t\t\r\n\t\treturn termList;\r\n\t}\r\n\r\n\tvar init = function() {\r\n\t\r\n\t    $(\"input[name^='RefinableString1'\").autocomplete({\r\n\t      source: function (request, response) {\r\n          \t loadTerms(request, \"Countries\").done(function (terms) { response(findTerms(terms, request.term)); });\r\n\t      }\r\n\t    });\r\n\t    \r\n\t    \/\/ insert css tag to load jquery.min.ui\r\n\t    var siteUrl = _spPageContextInfo.siteAbsoluteUrl;\r\n\r\n        var link1 = document.createElement('link');\r\n        link1.rel = 'stylesheet';\r\n        link1.type = 'text\/css';\r\n        link1.href = siteUrl + \"\/Style%20Library\/jquery-ui.min.css\";\r\n        document.getElementsByTagName('head')[0].appendChild(link1);\r\n    }\r\n    \r\n    return {\r\n        init: init\r\n    };\r\n}();\r\n\r\n<\/pre>\n<p>There&#8217;s a couple of things going on here. The\u00a0<strong>init<\/strong> method takes care of attaching the autocomplete to the textbox. Note that the name of the control is fixed (&#8220;RefinableString1&#8221;), as is the name of the termset (&#8220;Countries&#8221;). This is because there is no direct link between the refinable string and the termset it&#8217;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.<\/p>\n<p>Another thing the init method does is loading the <strong>jquery-ui CSS<\/strong> file. This is required to niceely style the autocomplete box. You can load CSS files in other ways, I like this because it&#8217;s clean, simple and only loads when needed.<\/p>\n<p>The\u00a0<strong>loadTerms<\/strong> function takes care of connecting to the sites termstore and querying the terms in the termset. It uses<strong> jQuery promises<\/strong> to wait for the load to finish.<\/p>\n<p>The\u00a0<strong>findTerms<\/strong> 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&#8217;t bother to at this time. Performance is quite ok so I think this is not really required unless you have really huge termsets.<\/p>\n<p>Finally, this array of term labels is passed to the\u00a0<strong>response\u00a0<\/strong>function of jQuery&#8217;s autocomplete. And voila:<\/p>\n<p><a href=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/refiner_sw.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-1556\" src=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/refiner_sw.png\" alt=\"refiner_sw\" width=\"188\" height=\"248\" \/><\/a><\/p>\n<p>We can now select switzerland from our autocomplete list and apply the value to the refiner!<\/p>\n<p><a href=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/refiner_switzerlandselected.png\"><img decoding=\"async\" loading=\"lazy\" class=\"alignnone size-full wp-image-1558\" src=\"http:\/\/blog.repsaj.nl\/wp-content\/uploads\/2015\/09\/refiner_switzerlandselected.png\" alt=\"refiner_switzerlandselected\" width=\"184\" height=\"206\" \/><\/a><\/p>\n<p>Notice how the &#8220;switzerland&#8221; selected value now appears? That&#8217;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.<\/p>\n<p>&nbsp;<\/p>\n<h2>Conclusion<\/h2>\n<p>Pretty neat to have autocomplete, yes? Do consider the following though. The autocomplete box will now use\u00a0all 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 \ud83d\ude42<\/p>\n<p>I&#8217;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&#8217;ll create a sample project for it.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>For a customer project I&#8217;m on we&#8217;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<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"jetpack_post_was_ever_published":false,"_jetpack_newsletter_access":"","jetpack_publicize_message":"","jetpack_is_tweetstorm":false,"jetpack_publicize_feature_enabled":true,"jetpack_social_post_already_shared":false,"jetpack_social_options":{"image_generator_settings":{"template":"highway","enabled":false}}},"categories":[34],"tags":[129,48,88,141,39],"jetpack_publicize_connections":[],"jetpack_featured_media_url":"","jetpack_shortlink":"https:\/\/wp.me\/p3KFR1-oZ","_links":{"self":[{"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/posts\/1549"}],"collection":[{"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/comments?post=1549"}],"version-history":[{"count":0,"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/posts\/1549\/revisions"}],"wp:attachment":[{"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/media?parent=1549"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/categories?post=1549"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/blog.repsaj.nl\/index.php\/wp-json\/wp\/v2\/tags?post=1549"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}