Replacing SharePoint 2013 Item Templates with AngularJS

After reading the title of this post, you’re probably asking yourself: What’s wrong with the display templates today in SharePoint 2013? Why would I want to replace them?

Now, I’ll preface this blog by saying I don’t think there’s anything inherently “wrong” with the way display templates work today. However, they do tend to struggle when it comes to really heavy data manipulation in JavaScript, such as filtering, grouping, and sharing data from multiple result sources – something that Angular does very nicely.

CLIENT REQUIREMENT

Our client was looking to customize many different Content Search Web Parts in order to build modular pages with entirely search-driven content. This included getting a large chunk of data from search and filtering / sorting by a custom Managed Metadata column in the display template – which made Angular the perfect candidate. While looking into adding Angular to a display template, I found an excellent blog post by Elio Struyf on the subject.

As great a foundation as this article is, I found myself looking for something “more” to reach my end goal. For my client, the ability to have more than one Angular web part on the page was crucial – which proved a little difficult without some extra intervention. Second, we needed the ability for web parts to “share” data – to be able to get data from two result sources and display them combined in one table. Plus, a little stronger architecture and mode code structure couldn’t hurt.

ARCHITECTURE, DATA FLOW AND IMPLEMENTATION

To expand on Elio’s points, I drafted a quick diagram to outline my code’s architecture and data flow:

image

Here we can see exactly how our code will work from start to finish. The diagram is split into two main areas, the top half being the JavaScript executed from the traditional SharePoint Display templates.

First, I added a line in each Control template that saved its ctx object as a global variable (firstCtx and secondCtx, created in WP_Control_FirstControltemplate.html and WP_Control_SecondControlTemplate.html, respectively). This effectively creates a snapshot of the ctx object at that point in time.

firstCtx = ctx;
secondCtx = ctx;

Additionally, I added a reference to the directives we’ll create in NgDirectives.js in each Control template. This will execute our directive once we’re all ready to go.

Next, I followed Elio’s instructions to create a “dummy” item template that contains the Managed Property Mappings to be used. I called this WP_Item_Angular_Blank.html.

Now we’ve completed our setup on the SharePoint Display Template end. On the bottom half of the diagram, our file invoking AngularJS, NgDirectives.js, has a custom directive for each control template:

//Create the Angular App
var app = angular.module("AngularApp", []);

//Create our first directive
app.directive('firstDirective', function() {
    return {
        restrict: 'A',
        scope: true,
        templateUrl: getCurrentSiteUrl() + '/_catalogs/masterpage/custom/ngTemplates/firstDirectiveTemplate.html',
        link: function(scope) {
            scope.items = getResultRows(firstCtx);
        }
    }
})
.directive('secondDirective', function () {
    return {
        restrict: 'A',
        scope: true,
        templateUrl: getCurrentSiteUrl() + '/_catalogs/masterpage/custom/ngTemplates/secondDirectiveTemplate.html',
        link: function (scope) {
            scope.firstItemSet = getResultRows(firstCtx)
            scope.secondItemSet = getResultRows(secondCtx);
        }
    }
});

Each Directive calls getResultRows, which works some of Elio’s magic to look into the Angular_Blank Item template, execute it, and grab the managed property mappings inside. Now, we’ve modified the ctx object in the exact same way that our Item Template would. So, in getResultRows(), we can now save our custom properties using $getItemValue() on the ctx.

        /* CUSTOM MAPPED PROPERTIES ARE GRABBED IN THE BLOCK BELOW */
        item.Created = $getItemValue(ctx, "Created").value;
        item.ListID = $getItemValue(ctx, "List ID").value;
        item.ListItemID = $getItemValue(ctx, "List Item ID").value;
        item.FileName = $getItemValue(ctx, "File Name").value;
        item.PictureURL = $getItemValue(ctx, "PictureURL").value;
        item.Path = $getItemValue(ctx, "Path").value;
        item.Description = $getItemValue(ctx, "Description").value;
        item.ContentType = $getItemValue(ctx, "Content Type").toString();
        item.Title = $getItemValue(ctx, 'Title').toString();
        item.FileExtension = $getItemValue(ctx, "FileExtension").toString();
        item.SecondaryFileExtension = $getItemValue(ctx, "SecondaryFileExtension").toString();
        item.FileSize = $getItemValue(ctx, "Size").toString();
        item.LastModifiedBy = $getItemValue(ctx, "Modified By").toString();

getResultRows() now returns the entire data set back to the directive, which is saved in the directive’s scope and can now be utilized in the Angular Template.

EXECUTION ON PAGE

Now, here’s one of the issues I encountered during implementation – executing the Angular directive. In Elio’s example, he only has one display template with Angular on the page, so execution isn’t a huge issue. However, with multiple Angular display templates, we ran into difficulty while running angular.bootstrap(document, [‘AngularApp’]) – sometimes it executed before all of the display templates were rendered. This led to our page being unstable and web parts randomly disappearing on refresh.

So, we implemented a simple system on our page. Each Control Template supports a PostRenderCallback that is executed when the control template has completed rendering on the page. Unfortunately, we can’t call our bootstrap here, because this only runs after a single Control Template renders – and they do not necessarily render in the same order every time. So, instead, we have a global variable created in HelperFunctions.js:

var TemplateRenderCount = 0;

Here, we can keep a running count of how many display templates have rendered on the page by this function call in each Control Template:

AddPostRenderCallback(ctx, function () {
     OnPostRenderDisplayTemplate();
});

This function lives in HelperFunctions.js, increments the global variable, and then checks to see if all of the templates have rendered.

function OnPostRenderDisplayTemplate() {
    TemplateRenderCount++;
    if (TemplateRenderCount >= TemplateRenderTrigger) {
        angular.bootstrap(document, ['AngularApp']);
    }
}

We also defined the number of control templates on the page using this global variable in the page layout:

var TemplateRenderTrigger = 2;

Our client found this acceptable, because these pages would be created initially during the build phase, and seldom changed (if ever). This is where my solution has room for improvement – I imagine you could use Javascript to determine the number of Content Search Web Parts on the page dynamically on load. However, that wasn’t in our scope, and therefore we left a hardcoded value on each page layout.

Put it all together, and the execution flow looks like this:

image

SHARING DATA

One of the other advantages brought about by this approach is that you can actually share data sets in between web parts and display templates. Since we saved firstCtx and secondCtx as global variables on the page, they don’t necessarily need to be used in their respective directives. Moreover, we can actually use them multiple times each, and we can use more than one ctx in each directive! Let’s say we want to use the data from firstCtx in firstDirective. Let’s say we also want to use firstCtx and secondCtx in secondDirective. That’s totally cool! To accomplish this, I just added a line to set firstItemSet in our secondDirective link function. So, it ends up looking like this:

        link: function (scope) {
            scope.firstItemSet = getResultRows(firstCtx)
            scope.secondItemSet = getResultRows(secondCtx);
        }

Which allows us to use it in our template, like this:

<h1>Angular Display Template - 2nd Template</h1>
    <div ng-repeat="item in firstItemSet">
        <h4>{{item.Title}}</h4>
        <div class="item-file-name">File Name: {{item.FileName}}</div>
        <div class="item-content-type">Content Type: {{item.ContentType}}</div>
        <div class="item-separator">* * * * * * * * * * * * * * * * * * </div>
    </div>
    <div ng-repeat="item in secondItemSet">
        <h4>{{item.Title}}</h4>
        <div class="item-file-name">File Name: {{item.FileName}}</div>
        <div class="item-modified-by">Modified By: {{item.LastModifiedBy}}</div>
        <div class="item-file-size">Size: {{item.FileSize}}</div>
        <div class="item-separator">* * * * * * * * * * * * * * * * * * </div>
    </div>

Notice how we now have two ng-repeat loops in our template, with two completely separate data sources. And our final results on the page looks something like this:

image

This opens up even more possibilities to leverage Angular with these data sets – we can filter and group based on a column, we can combine like items from multiple result sources, etc. As you can see, this provides us with a much more extensive code platform to work with than traditional SharePoint display templates.

SAMPLE CODE DOWNLOAD

If you’d like to download my full sample code, you can find it on GitHub.  I’d also like to credit Steve Samnadda and Dev Deol for working on this solution with me!

Questions? Want to tell your friends about how awesome this blog post is? Feel free to comment below or share this article.

Leave a Reply