Tallan's Technology Blog

Tallan's Top Technologists Share Their Thoughts on Today's Technology Challenges

Create a Multi Column Display Template for SharePoint

Brian Feldmann

SharePoint Display Templates make it fairly easy as a developer to set how search results are rendered for the end user. Just download a copy of an out-of-box display template, modify the markup, add some JavaScript, upload it to SharePoint and you’ve got yourself a new “template” for rendering search results. If you’re new to display templates take a look at SharePoint 2013 Design Manager display templates on MSDN.

As a developer, you have complete control over the way end users see and interact with search results. This post focuses on how you can create a column based display template. The end result will render items in a two column view where items flow top to bottom, left to right.

Although it’s possible to create “columns” using some CSS styling and a left to right design, this approach looks, to be blunt, terrible, especially when the result items don’t have a fixed height (see image below). Notice the big chunk of white space between the two items in the left column? UGLY.

MultiColumnDisplayTemplate-CSSOnly

Multi Column Display Template with CSS Only

The approach outlined in this post has a more fluid design for result items when there is no fixed height set (see image below).

MultiColumnDisplayTemplate

Multi Column Display Template with Markup

Setup

For this post, I’ve set up a custom list with a multi-line, rich text field. I’m going to surface the items from said list to my end users via a Content by Search web part. The end result will display the title field and the multi-line text field for each item in two columns; starting from the top of the left column moving down and over to the bottom of the right column. To keep things simple, I’m limiting the search results from a single list, but in a real world scenario I could use this web part along with my custom display templates to aggregate content from multiple lists that may exist in a number of different sites.

In order to accomplish the multiple column display, I will need to create two custom display templates, a Control Template and an Item Template. The column rendering code will be placed in the Control Template, this is where I’ll tell SharePoint two create two columns and how the items should flow. The Item Template is where I’ll tell SharePoint how to render each item.

Item Template

To create the Item Template, I start off by downloading a copy of the “Item_TwoLines.html” display template that SharePoint provides out-of-box.

First, I’ll update the display template <title/> element from <title>Two Lines</title> to <title>Multi Column - Item</title>. This will allow me to easily identify the display template in the content by search web part editor.

Second, I’ll modify the list of managed properties to include the multi-line text field that I mentioned earlier. I’ve already created a retrievable managed property named “MultiColumnLongDisplayText” and mapped it to the crawled property for the multi-line text field, so it’s ready for my display templates. I want this managed property to be available in the display template so that I can map the value in my markup. Managed Properties are beyond the scope of this post, but you can learn more about them here: https://technet.microsoft.com/en-us/library/jj219667.aspx

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Two lines</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:ManagedPropertyMapping msdt:dt="string">'Link URL'{Link URL}:'Path','Line 1'{Line 1}:'Title','Line 2'{Line 2}:'','FileExtension','SecondaryFileExtension'</mso:ManagedPropertyMapping>
<mso:MasterPageDescription msdt:dt="string">This Item Display Template will show a small thumbnail icon next to a hyperlink of the item title, with an additional line that is available for a custom managed property.</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties>
</xml><![endif]-->
</head>

The last thing I need to do in the Item Template is set how each individual item will render. To do this I strip out everything inside of the <div id="TwoLines" /> element and replace it with my own markup. My new markup looks like this:

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

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:ManagedPropertyMapping msdt:dt="string">'Link URL'{Link URL}:'Path','Line 1'{Line 1}:'Title','Line 2'{Line 2}:'MultiColumnLongDisplayText','FileExtension','SecondaryFileExtension'</mso:ManagedPropertyMapping>
<mso:MasterPageDescription msdt:dt="string">This Item Display Template will show a small thumbnail icon next to a hyperlink of the item title, with an additional line that is available for a custom managed property.</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106603</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties>
</xml><![endif]-->
</head>

<body>
    <!--
            Warning: Do not try to add HTML to this section. Only the contents of the first <div>
            inside the <body> tag will be used while executing Display Template code. Any HTML that
            you add to this section will NOT become part of your Display Template.
    -->
    <script>
        $includeLanguageScript(this.url, "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
		$includeCSS(this.url, "~sitecollection/Style Library/MCDT/MCDT-styles.css");
    </script>

    <div id="TwoLines">
	<!--#_
		var url = $getItemValue(ctx, "Link URL"),
			title = $getItemValue(ctx, "Line 1"),
			body = $getItemValue(ctx, "Line 2"),
			readmore = false;

		url.overrideValueRenderer($urlHtmlEncodeValueObject);
		title.overrideValueRenderer($contentLineText);
		body.overrideValueRenderer($contentLineText);

		if (!body.isEmpty && body.value.length > 750) {
			body.value = body.value.substring(0, 750) + '...';
			readmore = true;
		}
	_#-->
		<div class="list-item">
			<h4><a href="_#=url=#_">_#=title=#_</a></h4>
			<div class="summary">
				_#=body=#_
	<!--#_
				if (readmore) {
	_#-->
				<a href="_#=url=#_">read more</a>
	<!--#_
				}
	_#-->
			</div>
		</div>
    </div>
</body>
</html>

The Item Display Template is fairly straight forward. There’s some JavaScript to tell SharePoint what values to render and some html markup to to tell it how to render those values.

Control Template

The Control Template is where the column logic goes. This file is what tells SharePoint that two columns need to be set and how many items go in each column. I used the “Control_List.html” control template as my basis.

First, I modified the <title/> element of this file from <title>List</title> to <title>Multi Column – List</title>.

Next, I stripped out the markup in the <div class="Control_List" /> element and replaced it with my own. Let’s examine the modifications piece by piece.

List Render Wrapper Function

Control Templates define the container for the individual search results. These files have two very important roles. One is to tell SharePoint how the container (or wrapper) should look for the collection of search results. The second is to tell SharePoint what Item Template to use when it renders the individual items within the control. This is where the ListRenderRenderWrapper comes in (and no, that’s not a typo Smile).  The ListRenderRenderWrapper gets called when each item is converted to an html string (based on the Item Template). If you used the Control_List.html like I did, it looks something like this:

var ListRenderRenderWrapper = function(itemRenderResult, inCtx, tpl)
{
    var iStr = [];
    iStr.push('<li>');
    iStr.push(itemRenderResult);
    iStr.push('</li>');
    return iStr.join('');
}
ctx['ItemRenderWrapper'] = ListRenderRenderWrapper;

Notice the <li/> tags that get pushed into the array? The default control template container element is a <ul/>, so it makes sense for the individual list items to be wrapped in an <li/>, but in my case I changed the <ul/> to a <div/> and removed the <li/> from the function.  So my updated function now looks like this:

var ListRenderRenderWrapper = function(itemRenderResult, inCtx, tpl) {
	var iStr = [];
	iStr.push(itemRenderResult);
	return iStr.join('');
}
ctx['ItemRenderWrapper'] = ListRenderRenderWrapper;
Render Column Logic

The next thing I need to do is determine the number of items that have been returned by the search query and decide how to divide up the columns. To do that I grab the number of results from the ctx object and then divide that by the number of columns we want to create. For this post I’m creating two columns.


var resultCount = ctx.ListData.ResultTables.length > 0 ? ctx.ListData.ResultTables[0].ResultRows.length : 0,
	leftColumnCount = resultCount > 1 ? Math.ceil(resultCount / 2) : resultCount;

I also need to create a function that renders the result item groups for each column into actual html markup. This is done by the following code:

var RenderGroupsOverride = function (baseCtx, startIndex, maxRows) {
	var newCtx = Object.assign({}, baseCtx);
	newCtx.ListData = JSON.parse(JSON.stringify(baseCtx.ListData));
	if (resultCount > 0) {
		if (!maxRows)
			maxRows = resultCount - startIndex;
		newCtx.ListData.ResultTables[0].ResultRows = newCtx.ListData.ResultTables[0].ResultRows.splice(startIndex, maxRows);
	}
	return baseCtx.RenderGroups(newCtx);
}

The above function makes a copy of the original ctx object. This object contains a ton of information such as the collection of items in the result set, the display templates used for rendering and much more. The key object that I’m concerned with here is the baseCtx.ListData property. In order to create a slimmed down result set for each column, I have to create a Deep Copy of the baseCtx.ListData property. To do this I used Object.assign to create a copy of the baseCtx object. It’s important to note that Object.assign creates a Shallow Copy of the object, so if I were to modify the newCtx.ListData property it would also modify the original baseCtx.ListData property, which is not something I want to do. To prevent that from happening I create a deep copy of the property by using the built in JSON functions. First I serialize the ListData to a string and then I parse it back to an object, which in effect creates a deep copy. The reason I can’t do that from the start is because there are functions in the ctx object, so errors will be thrown if I try to serialize it.

Important: Object.assign is not compatible with Internet Explorer. If you need to support IE consider using something like jQuery.extend instead.

Now that I have the original baseCtx object copied, I can go ahead and remove the result items that do not belong to the column. For that I use the splice JavaScript function. The last thing to do is call the built in RenderGroups function on the original baseCtx object and pass in the newly created newCtx object. Calling the RenderGroups function converts the object into the html markup specified in the Item Template.

Column Markup

Now that all the column logic is in place I can add the markup and tell SharePoint how to render the columns.

    <div class="row">
		<div class="col-xs-10">
			_#= RenderGroupsOverride(ctx, 0, leftColumnCount) =#_
		</div>
<!--#_
		if (leftColumnCount < resultCount) {
_#-->
		<div class="col-xs-10">
			_#= RenderGroupsOverride(ctx, leftColumnCount) =#_
		</div>
<!--#_
		}
_#-->
	</div>

The markup specifies two columns, both of which are contained in a <div class="col-xs-10" /> element. Inside of each column I call the RenderGroupsOverride function, passing in the original ctx object, the start index and the maximum number of items to render in the column. The second column is wrapped in an if statement which only calls the RenderGroupsOverride function if the number of results in the left column is less than the total number of results in the original result set.

The complete Control Template looks like this:

<html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
<head>
<title>Multi Column - List</title>

<!--[if gte mso 9]><xml>
<mso:CustomDocumentProperties>
<mso:TemplateHidden msdt:dt="string">0</mso:TemplateHidden>
<mso:MasterPageDescription msdt:dt="string">This is the default Control Display Template that will list the items. It does not allow the user to page through items.</mso:MasterPageDescription>
<mso:ContentTypeId msdt:dt="string">0x0101002039C03B61C64EC4A04F5361F385106601</mso:ContentTypeId>
<mso:TargetControlType msdt:dt="string">;#Content Web Parts;#</mso:TargetControlType>
<mso:HtmlDesignAssociated msdt:dt="string">1</mso:HtmlDesignAssociated>
</mso:CustomDocumentProperties>
</xml><![endif]-->
</head>

<body>
    <!--
            Warning: Do not try to add HTML to this section. Only the contents of the first <div>
            inside the <body> tag will be used while executing Display Template code. Any HTML that
            you add to this section will NOT become part of your Display Template.
    -->
    <script>
        $includeLanguageScript(this.url, "~sitecollection/_catalogs/masterpage/Display Templates/Language Files/{Locale}/CustomStrings.js");
		$includeCSS(this.url, "~sitecollection/Style Library/MCDT/bootstrap.min.css");
    </script>

    <div id="Control_List">

<!--#_
		if (!$isNull(ctx.ClientControl) &&
			!$isNull(ctx.ClientControl.shouldRenderControl) &&
			!ctx.ClientControl.shouldRenderControl()) {
			return "";
		}
		ctx.ListDataJSONGroupsKey = "ResultTables";
		var $noResults = Srch.ContentBySearch.getControlTemplateEncodedNoResultsMessage(ctx.ClientControl);

		var noResultsClassName = "ms-srch-result-noResults";

		var ListRenderRenderWrapper = function(itemRenderResult, inCtx, tpl) {
			var iStr = [];
			iStr.push(itemRenderResult);
			return iStr.join('');
		}
		ctx['ItemRenderWrapper'] = ListRenderRenderWrapper;

		var resultCount = ctx.ListData.ResultTables.length > 0 ? ctx.ListData.ResultTables[0].ResultRows.length : 0,
			leftColumnCount = resultCount > 1 ? Math.ceil(resultCount / 2) : resultCount;

		var RenderGroupsOverride = function (baseCtx, startIndex, maxRows) {
			var newCtx = Object.assign({}, baseCtx);
			newCtx.ListData = JSON.parse(JSON.stringify(baseCtx.ListData));
			if (resultCount > 0) {
				if (!maxRows)
					maxRows = resultCount - startIndex;
				newCtx.ListData.ResultTables[0].ResultRows = newCtx.ListData.ResultTables[0].ResultRows.splice(startIndex, maxRows);
			}
			return baseCtx.RenderGroups(newCtx);
		}
_#-->
    <div class="row">
		<div class="col-xs-10">
			_#= RenderGroupsOverride(ctx, 0, leftColumnCount) =#_
		</div>
<!--#_
		if (leftColumnCount < resultCount) {
_#-->
		<div class="col-xs-10">
			_#= RenderGroupsOverride(ctx, leftColumnCount) =#_
		</div>
<!--#_
		}
_#-->
	</div>
<!--#_
	if (ctx.ClientControl.get_shouldShowNoResultMessage())
	{
_#-->
        <div class="_#= noResultsClassName =#_">_#= $noResults =#_</div>
<!--#_
	}
_#-->

    </div>
</body>
</html>

_________________________________________________________________________________________

To learn more about SharePoint and how Tallan can help you build enterprise productivity solutions to help you meet your business goals, CLICK HERE.

No comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

\\\