Once an arcane Art whose brooding mysteries eluded the most learned and powerful gurus, custom SharePoint search is vastly more powerful and simple in SP 2013. Through display templates, we can render arbitrary HTML / JS for both search refiners and results. The possibilities are endless. Pie-chart refiners? Hands-free refining via neural matrix uplink? Refinement with actual pies? Someone’s probably already blogged it.
Still, creating my first custom refiner was a daunting task—especially with the profusion of sophisticated examples. Here I’ll go over how to make a very simple drop-down refiner that acts more like a plain ol' filter.
The goal is to let users filter on Approval Status (the ows__ModerationStatus field).
There are a bunch of statuses, but most users only care about “published” vs “non-published”. So let’s build a refiner with just those options.
Active will filter for Approved status, and Inactive will be everything else (Draft, Pending, Scheduled, etc). Note that the value in Moderation Status is actually an integer which corresponds to these statuses.
I'm also assuming Search is configured and working properly, and you have a page with Search Results and Search Refinement webparts.
Note that there is a HTML and JS file for each template. The JS is a SharePoint generated file. We will only ever work with the HTML. Download a copy of Filter_Default.html.
Crack that open in a text editor and remove everything between the body tags. You will end up with just this:
Update the title. Inside the body, we'll add a couple DIVs and a static drop down box:
Edit your Refinement webpart, and hit Choose Refiners. Add the ModerationStatus managed property as a refiner, and change its Display Template. Note that this was the title we set in the HTML.
Hit OK and save the page. If you see a drop down in the refiners panel, we're in business. Sort of. Of course, it doesn't do anything yet...
First, wire up the dropdown box's onchange event. We will create an applyRefiner(val, refinerCtrl) method to apply the selected refiner. The javascript code for this method needs to go into an external JS file.
Note: this file can not have the same name as the display template. That is reserved for the SharePoint generated JS file. I named the example Filter_ModerationStatus_functions.js.
Add a script block beneath the body to reference this external file.
Create Filter_ModerationStatus_functions.js, and add the code below. Recall that val is the option value that's just been selected.
First, we clear any active ModerationStatus refiners by setting it to NULL. Then, for Active, add a refinement for ModerationStatus = 0, which stands for Approved. Inactive actually corresponds to a set of ModerationStatuses, so we use addRefinementFiltersWithOp with an OR operation to tell SharePoint to match on any of the statuses in the array.
There's scant documentation on MSDN (and by scant I mean none) for the refiner control, but check out Elio Struyf's blog post for other refinement methods and more examples.
Upload both the HTML and JS files, and reload the search result page. Now, when you change the dropdown, the search results should be getting refined!
To fix this, we have to add some javascript to the display template. This is different from the javascript in the external file. Javascript inside the display template controls what gets rendered. The script itself is not emitted on to the page. Therefore, javascript that's invoked by the controls on the page must live in an external file.
Within the template, all javascript statements must be enclosed in special tags: <!--#_ _#-->. Javascript variables can also be emitted on to the page when wrapped in these tags: _#= =#_.
We'll make a simple method to render the dropdown option based on it's selected state:
The javascript lines and variables must be wrapped before being added to the template:
Finally, we replace the hard-coded options in the dropdown with calls to addRefiner. For each option, we ask the refinement control if the corresponding filters are active by calling hasAllRefinementFilters.
For completion, we can also add a title at the top.
Still, creating my first custom refiner was a daunting task—especially with the profusion of sophisticated examples. Here I’ll go over how to make a very simple drop-down refiner that acts more like a plain ol' filter.
The goal is to let users filter on Approval Status (the ows__ModerationStatus field).
There are a bunch of statuses, but most users only care about “published” vs “non-published”. So let’s build a refiner with just those options.
Active will filter for Approved status, and Inactive will be everything else (Draft, Pending, Scheduled, etc). Note that the value in Moderation Status is actually an integer which corresponds to these statuses.
Assumptions
Though ows__ModerationStatus field is an OOB field, it is not by default a search Managed Property. I’m assuming in this example that it’s already been mapped to a Managed Property called ModerationStatus.I'm also assuming Search is configured and working properly, and you have a page with Search Results and Search Refinement webparts.
Custom Display Template
Let's start simple by building a refiner with a static dropdown box. The OOB refiner display templates live in the Master Page Gallery, under /_catalogs/masterpage/Display Templates/Filters.Note that there is a HTML and JS file for each template. The JS is a SharePoint generated file. We will only ever work with the HTML. Download a copy of Filter_Default.html.
Crack that open in a text editor and remove everything between the body tags. You will end up with just this:
<html xmlns:mso="urn:schemas-microsoft-com:office:office"
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Refinement Item</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
</body>
</html>
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Refinement Item</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
</body>
</html>
Update the title. Inside the body, we'll add a couple DIVs and a static drop down box:
<html xmlns:mso="urn:schemas-microsoft-com:office:office"
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<div id="ModerationStatusRefinement">
<div id="Container">
<select id="statusDdl">
<option value="0">ALL</option>
<option value="1">Active</option>
<option value="2">Inactive</option>
</select>
</div>
</div>
</body>
</html>
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<div id="ModerationStatusRefinement">
<div id="Container">
<select id="statusDdl">
<option value="0">ALL</option>
<option value="1">Active</option>
<option value="2">Inactive</option>
</select>
</div>
</div>
</body>
</html>
Test Drive
Now, let's see it in action! Save the file as Filter_ModerationStatus.html, and upload it to the Master Page gallery.Edit your Refinement webpart, and hit Choose Refiners. Add the ModerationStatus managed property as a refiner, and change its Display Template. Note that this was the title we set in the HTML.
Hit OK and save the page. If you see a drop down in the refiners panel, we're in business. Sort of. Of course, it doesn't do anything yet...
Adding Refinement
To make the refiner, you know... refine stuff, we have to update the active refiners when the dropdown value changes. This is done through javascript by invoking methods on the refiner control.First, wire up the dropdown box's onchange event. We will create an applyRefiner(val, refinerCtrl) method to apply the selected refiner. The javascript code for this method needs to go into an external JS file.
Note: this file can not have the same name as the display template. That is reserved for the SharePoint generated JS file. I named the example Filter_ModerationStatus_functions.js.
Add a script block beneath the body to reference this external file.
<html xmlns:mso="urn:schemas-microsoft-com:office:office"
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<script>
$includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
</script>
<div id="ModerationStatusRefinement">
<div id="Container">
<select id="statusDdl"
onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
<option value="0">ALL</option>
<option value="1">Active</option>
<option value="2">Inactive</option>
</select>
</div>
</div>
</body>
</html>
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<script>
$includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
</script>
<div id="ModerationStatusRefinement">
<div id="Container">
<select id="statusDdl"
onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
<option value="0">ALL</option>
<option value="1">Active</option>
<option value="2">Inactive</option>
</select>
</div>
</div>
</body>
</html>
Create Filter_ModerationStatus_functions.js, and add the code below. Recall that val is the option value that's just been selected.
First, we clear any active ModerationStatus refiners by setting it to NULL. Then, for Active, add a refinement for ModerationStatus = 0, which stands for Approved. Inactive actually corresponds to a set of ModerationStatuses, so we use addRefinementFiltersWithOp with an OR operation to tell SharePoint to match on any of the statuses in the array.
function applyRefiner(val, refinementCtrl) {
// Clear refiners
refinementCtrl.updateRefiners( { 'ModerationStatus' : null } );
if (val == 1) // Active --> Approved
refinementCtrl.addRefinementFilter('ModerationStatus', '0');
else if (val == 2) // Inactive --> Rejected, Pending, Draft, Scheduled
refinementCtrl.addRefinementFiltersWithOp(
{ 'ModerationStatus' : ['1','2','3','4'] },
'or'
);
}
// Clear refiners
refinementCtrl.updateRefiners( { 'ModerationStatus' : null } );
if (val == 1) // Active --> Approved
refinementCtrl.addRefinementFilter('ModerationStatus', '0');
else if (val == 2) // Inactive --> Rejected, Pending, Draft, Scheduled
refinementCtrl.addRefinementFiltersWithOp(
{ 'ModerationStatus' : ['1','2','3','4'] },
'or'
);
}
There's scant documentation on MSDN (and by scant I mean none) for the refiner control, but check out Elio Struyf's blog post for other refinement methods and more examples.
Upload both the HTML and JS files, and reload the search result page. Now, when you change the dropdown, the search results should be getting refined!
Finishing Touches
You probably noticed the Status selection doesn't stay selected and gets reset (though the search results are being refined). This is because we didn't take into account which option is already selected when rendering the dropdown.To fix this, we have to add some javascript to the display template. This is different from the javascript in the external file. Javascript inside the display template controls what gets rendered. The script itself is not emitted on to the page. Therefore, javascript that's invoked by the controls on the page must live in an external file.
Within the template, all javascript statements must be enclosed in special tags: <!--#_ _#-->. Javascript variables can also be emitted on to the page when wrapped in these tags: _#= =#_.
We'll make a simple method to render the dropdown option based on it's selected state:
function addRefiner(val, text, selected) {
if (selected) {
<option selected="selected" value="val">text</option>
} else {
<option value="val">text</option>
}
}
if (selected) {
<option selected="selected" value="val">text</option>
} else {
<option value="val">text</option>
}
}
The javascript lines and variables must be wrapped before being added to the template:
<!--#_
function addRefiner(val, text, selected) {
if (selected) {
_#-->
<option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_
} else {
_#-->
<option value="_#= val =#_">_#= text =#_</option>
<!--#_
}
}
_#-->
function addRefiner(val, text, selected) {
if (selected) {
_#-->
<option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_
} else {
_#-->
<option value="_#= val =#_">_#= text =#_</option>
<!--#_
}
}
_#-->
Finally, we replace the hard-coded options in the dropdown with calls to addRefiner. For each option, we ask the refinement control if the corresponding filters are active by calling hasAllRefinementFilters.
For completion, we can also add a title at the top.
<html xmlns:mso="urn:schemas-microsoft-com:office:office"
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<script>
$includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
</script>
<div id="ModerationStatusRefinement">
<div id="Container">
<div style="font-size:13pt; margin-bottom: 10px">
_#= Srch.Refinement.getRefinementTitle(ctx.RefinementControl) =#_
</div>
<select id="statusDdl"
onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
<!--#_
addRefiner(0, 'All', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['']));
addRefiner(1, 'Active', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['0']));
addRefiner(2, 'Inactive', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['1', '2','3','4']));
_#-->
</select>
</div>
xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Moderation Status Refinement</title>
<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:CompatibleManagedProperties msdt:dt="string"></mso:CompatibleManagedProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:CompatibleSearchDataTypes msdt:dt="string"></mso:CompatibleSearchDataTypes>
<mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106604</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Refinement;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties></xml><![endif]-->
</head>
<body>
<script>
$includeScript(this.url, "~sitecollection/_catalogs/masterpage/display templates/filters/filter_moderationstatus_functions.js");
</script>
<div id="ModerationStatusRefinement">
<div id="Container">
<div style="font-size:13pt; margin-bottom: 10px">
_#= Srch.Refinement.getRefinementTitle(ctx.RefinementControl) =#_
</div>
<select id="statusDdl"
onchange="javascript:applyRefiner(this.value, $getClientControl(this));">
<!--#_
addRefiner(0, 'All', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['']));
addRefiner(1, 'Active', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['0']));
addRefiner(2, 'Inactive', ctx.ClientControl.hasAllRefinementFilters('ModerationStatus', ['1', '2','3','4']));
_#-->
</select>
</div>
<!--#_
function addRefiner(val, text, selected) {
if (selected) {
_#-->
<option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_
} else {
_#-->
<option value="_#= val =#_">_#= text =#_</option>
<!--#_
}
}
_#-->
</div>
</body>
</html>function addRefiner(val, text, selected) {
if (selected) {
_#-->
<option selected="selected" value="_#= val =#_">_#= text =#_</option>
<!--#_
} else {
_#-->
<option value="_#= val =#_">_#= text =#_</option>
<!--#_
}
}
_#-->
</div>
</body>