Search Display Templates Made Easy w/ Knockout

Thursday, June 30, 2016

3

I first started using Knockout JS in display templates for a solution that required complex interactive search results.  But I found that Knockout made working with display templates SO much easier in general, it's now my go-to solution for building any custom template!

The inline JavaScript / HTML mashup syntax that display templates use turns the simplest requirements into a giant, unreadable mess of <!--#_ _#--> and _#= =#_ tags.

Incorporating Knockout enables us to:

  • Write clean markup with easy-to-read databinding
  • Open the door to more complex interactive solutions

Why Knockout JS, specifically?  Mostly because it's what I'm most familiar with.  I'm sure other frameworks like ReactJS or AngularJS could be use with similar benefits.  I like Knockout because it's trivially simple to set up and get started with.  For simple templates, the data-binding feature is all we'll need.  But having Knockout also opens the door to dependency-tracking, 2-way data binding, event handling, custom components, and other cool stuff down the road.

Include KnockoutJS

First, we need to include the KnockoutJS framework on the site.  It is a single, standalone JavaScript file which you can download from http://knockoutjs.com.

Upload it some where on SharePoint (eg Master Page Gallery or Site Assets), then add a reference to it in the Master Page:

<!--MS:<SharePoint:ScriptLink language="javascript" name="~SiteCollection/_catalogs/masterpage/js/knockout-3.4.0.js" OnDemand="false" runat="server" Localizable="false">-->
<!--ME:</SharePoint:ScriptLink>-->

Display Template Markup

Full code example is at the end of this article.  Here I'll break down the template and explain what each part does:

Pre-Render Javascript (lines 18-22)


This JavaScript in the special inline JavaScript/HTML syntax runs before the markup is rendered. Here, we simply save a reference to the current search result item, and generate a unique ID.

Markup (lines 24-33)


This is where the HTML markup and Knockout data-bindings for the current item are defined.  This markup will be emitted to the search results page.

Note on line 24 where we plug the unique container ID generated above into the markup with _=# containerId #=_.  This is the only place where we need to use the _#= =#_ syntax, as the ID has to be inserted before applying Knockout data-binding.

The rest is arbitrary HTML and Knockout bindings!  Go crazy!  For those unfamiliar with Knockout, $root refers to the root data model object that the template is bound to.  Here, we'll bind the template to the search result item, ctx.CurrentItem.  So, any property available on the item can be used in the template.  Remember that only Managed Properties that have been called out in ManagedPropertyMapping (line 8) are available.

Post Render - Apply Data-bindings (lines 35-40)


Finally, we register a Post Render callback method which executes after the template markup has been emitted.

We tell Knockout to apply the data-bindings by calling ko.applyBindings.  We pass it the search result item to map from ($root in the markup bindings), as well as the HTML element containing the template to bind to.


<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
  <title>My Custom Search Result Item</title>

  <!--[if gte mso 9]><xml>
  <mso:CustomDocumentProperties>
  <mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
  <mso:ManagedPropertyMapping msdt:dt="string">'Title','Path','Author'</mso:ManagedPropertyMapping>
  <mso:MasterPageDescription msdt:dt="string"></mso:MasterPageDescription>
  <mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
  <mso:TargetControlType msdt:dt="string">;#SearchResults;#</mso:TargetControlType>
  <mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
  </mso:CustomDocumentProperties>
  </xml><![endif]-->
</head>
<body>
  <div id="item">
    <!--#_
    var encodedId = $htmlEncode(ctx.ClientControl.get_nextUniqueId());
    var containerId = "MyCustomItem_" + encodedId;
    var currentItem = ctx.CurrentItem;
    _#-->

    <div id="_#=containerId=#_">
      <div>
        <span><b>Title:</b></span>
        <span data-bind="text: $root.Title"></span>
      </div>
      <div>
        <span><b>Path:</b></span>
        <span data-bind="text: $root.Path"></span>
      </div>
    </div>

    <!--#_
    AddPostRenderCallback(ctx, function()
    {
      ko.applyBindings(currentItem, document.getElementById(containerId));
    });
    _#-->
  </div>
</body>
</html>


3 comments:

Post a Comment