Showing posts with label CSR. Show all posts
Showing posts with label CSR. Show all posts

Thursday, November 16, 2017

How to add temporary columns to a List View using CSR, in order to display dynamic information not stored in the list.

I have found it useful sometimes to use the CSR framework to display extra information in a List View that isn't necessarily stored on each list item.  This is especially true of values that are related to the list item but may be dynamic or conditional in some way, and it makes more sense to evaluate those conditions using JavaScript at render time than to use something like, say, a Calculated Column, because a Calculated Column is only re-calculated when the item is updated.

My original approach to implementing this kind of thing was to add a column to the list as a place-holder for where my dynamic value would be rendered, and then link the CSR override script to that field using the field's JSLink property.

However, at the recent SharePointFest DC conference, I was at a talk about CSR, and it was mentioned that, from a data integrity standpoint, that is a bad practice because you are essentially adding a field to the list in which you are never going to store any data.  It was also mentioned that you could probably just add a field directly in the view you do your rendering on.

I thought that sounded reasonable, and was an interesting approach, so I played around with the idea for a little bit and came up with this general pattern for doing just that.

And instead of attaching to the JSLink property of a field, these scripts are intended to be added directly to the list Views through the List View Web Part's JSLink property (which is exposed in the web part's Editor pane).

During my experimentation, I was trying to add the temporary column in OnPreRender, but still register a field override for that field in the normal manner.  That turned out to be problematic, so eventually I settled on using OnPreRender to add the temporary field to the view, and using OnPostRender to do all the calculations and render the final HTML.

Here's a code snippet that outlines the basic pattern, which I have commented heavily so you can follow along:

  1. var DEC = DEC || {};
  2.  
  3. DEC.FakeField = (function () {
  4.  
  5.     function addTemporaryField(ctx) {
  6.         // make sure we haven't added the field already,
  7.         // in case OnPreRender fires twice, which it does sometimes
  8.         if (ctx.ListSchema.Field.filter(function (fld) {
  9.             return fld.Name == 'FakeField';
  10.         }).length == 0) {
  11.  
  12.             // create the field schema object to insert
  13.             var FakeFieldObj = {
  14.                 AllowGridEditing: 'FALSE',
  15.                 DisplayName: 'Fake Field',
  16.                 RealFieldName: 'FakeField',
  17.                 Name: 'FakeField',
  18.                 FieldType: 'Text',
  19.                 Type: 'Text',
  20.                 Filterable: 'FALSE',
  21.                 Sortable: 'FALSE',
  22.                 ReadOnly: 'TRUE',
  23.             };
  24.  
  25.             // find the index of the field to insert next to,
  26.             // based on that field's DISPLAY name.
  27.             var insertIdx = ctx.ListSchema.Field.map(function (fld) {
  28.                 return fld.DisplayName;
  29.             }).indexOf('Some Field Name');
  30.  
  31.             // if you want to insert *before* the field do not add anything
  32.             // but, if you want to insert *after* the field, add 1
  33.             insertIdx++;
  34.  
  35.             // insert your fake field schema object into the list schema
  36.             ctx.ListSchema.Field.splice(insertIdx, 0, FakeFieldObj);
  37.         }
  38.     }
  39.  
  40.     function renderFakeFieldValues(ctx) {
  41.         // get the index of the fake field column in the table
  42.         // by looking for the header with the DISPLAY NAME of our fake field,
  43.         // so we know where to insert the final rendered value for each item
  44.         var colIndex = 0;
  45.         var headers = document.querySelectorAll('.ms-listviewtable th');
  46.         var len = headers.length;
  47.         for (var idx = 0; idx < len; idx++) {
  48.             if (headers[idx].innerHTML.indexOf('Fake Field') != -1) {
  49.                 colIndex = idx;
  50.             }
  51.         };
  52.  
  53.         // go through the rows and do whatever calculations we need
  54.         // to get the value we want to render in the added field.
  55.         // at this point ctx.ListData.Row represents a collection
  56.         // of list items for all the rows currently visible in the view.
  57.         ctx.ListData.Row.forEach(function (listItem) {
  58.  
  59.             // here, listItem will have all the data of all
  60.             // the columns visible in the view, which we
  61.             // can access by listItem.FieldInternalName
  62.  
  63.             // we can then use those values to do our conditional formatting
  64.             // and calculate / generate the value to render in the fake field
  65.             var helloWorldHtml = '<span>Hello ' + listItem.WorldField + '</span>';
  66.  
  67.             // find the row in the table that corresponds to this list item
  68.             var iid = GenerateIIDForListItem(ctx, listItem);
  69.             var row = document.getElementById(iid);
  70.  
  71.             // insert the value we want in the fake field column
  72.             // of the row representing this list item
  73.             row.children[colIndex].innerHTML = helloWorldHtml;
  74.         });
  75.     }
  76.  
  77.     return {
  78.         render: function () {
  79.             SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  80.                 OnPreRender: addTemporaryField,
  81.                 OnPostRender: renderFakeFieldValues
  82.             });
  83.         }
  84.     }
  85. })();
  86.  
  87. RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~site/SiteAssets/Scripts/AddFakeField.js"), DEC.FakeField.render);
  88. DEC.FakeField.render();

So where is this useful?  Well, let's say you want to highlight list items that are overdue.  Sure, you could come up with a rendering override for the due date field itself, and either render the text in red, or add a red background, or something like that.  But what if you wanted to display a message to the users that spelled out exactly how many days overdue the item was, like "XX days overdue".

The following code snippet shows how to do exactly that:

  1. var DEC = DEC || {};
  2.  
  3. DEC.DaysOverdueField = (function () {
  4.  
  5.     function addTemporaryField(ctx) {
  6.  
  7.         if (ctx.ListSchema.Field.filter(function (fld) {
  8.             return fld.Name == 'DaysOverdueField';
  9.         }).length == 0) {
  10.  
  11.             var daysOverdueFieldObj = {
  12.                 AllowGridEditing: 'FALSE',
  13.                 DisplayName: 'Days Overdue',
  14.                 RealFieldName: 'DaysOverdueField',
  15.                 Name: 'DaysOverdueField',
  16.                 FieldType: 'Text',
  17.                 Type: 'Text',
  18.                 Filterable: 'FALSE',
  19.                 Sortable: 'FALSE',
  20.                 ReadOnly: 'TRUE',
  21.             };
  22.  
  23.             // in this case i am going to show the "XX days overdue"
  24.             // message directly next to the due date field
  25.             var insertIdx = ctx.ListSchema.Field.map(function (fld) {
  26.                 return fld.DisplayName;
  27.             }).indexOf('Due Date');
  28.  
  29.             // insert *after* the due date field
  30.             insertIdx++;
  31.  
  32.             ctx.ListSchema.Field.splice(insertIdx, 0, daysOverdueFieldObj);
  33.         }
  34.     }
  35.  
  36.     function calculateOverdueDays(ctx) {
  37.         // get the index of the "Days Overdue" column in the table
  38.         var colIndex = 0;
  39.         var headers = document.querySelectorAll('.ms-listviewtable th');
  40.         var len = headers.length;
  41.         for (var idx = 0; idx < len; idx++) {
  42.             if (headers[idx].innerHTML.indexOf('Days Overdue') != -1) {
  43.                 colIndex = idx;
  44.             }
  45.         };
  46.  
  47.         // go through the rows and display an overdue message if it is overdue
  48.         ctx.ListData.Row.forEach(function (listItem) {
  49.             // get the due date
  50.             var duedate = new Date(listItem.DueDate);
  51.  
  52.             // use the built in GetDaysAfterToday function to calculate
  53.             // how many days overdue an item is.  if it is truly overdue,
  54.             // this function will return a negative number.
  55.             var overdueDays = GetDaysAfterToday(duedate);
  56.             if (overdueDays < 0) {
  57.  
  58.                 // change the negative to a positive
  59.                 overdueDays = Math.abs(overdueDays);
  60.  
  61.                 // find the row in the table that corresponds to this list item
  62.                 var iid = GenerateIIDForListItem(ctx, listItem);
  63.                 var row = document.getElementById(iid);
  64.  
  65.                 // insert the overdue message in the <td> element in our temporary "Days Overdue" column
  66.                 row.children[colIndex].innerHTML = '<span style="color:red;">' + overdueDays + ' days overdue</span>';
  67.             }
  68.         });
  69.     }
  70.  
  71.     return {
  72.         render: function () {
  73.             SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  74.                 OnPreRender: addTemporaryField,
  75.                 OnPostRender: calculateOverdueDays
  76.             });
  77.         }
  78.     }
  79. })();
  80.  
  81. RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~site/SiteAssets/Scripts/ShowDaysOverdue.js"), DEC.DaysOverdueField.render);
  82. DEC.DaysOverdueField.render();

So, that is how you can add a temporary column to a list view, and then access it to add dynamic information related to the list items into the view.  As requested, here is a screen shot of the "days overdue" code in action.  The column "Days Overdue" does not exist on this list:



In a subsequent post I will show how to use this technique to do something a little more complex: display file counts next to folders in a Document Library view that includes all files within all subfolders within any particular folder.

Sunday, May 1, 2016

A Better Pattern for Client Side Rendering Scripts

Recently I did some work for a client that required combining multiple List View Web Parts on one page, and using Client Side Rendering (CSR) to apply custom styling to each of the the various views. The site was a Team Site that had Minimal Download Strategy (MDS) enabled, and this led to some interesting challenges. The MDS feature is supposed to speed up browsing by having the client only process the differences between the current page and the new page. However, this can lead to situations where custom CSR scripts do not get executed correctly.

In a nutshell, MDS registers JavaScript files as it downloads/executes them, and on a subsequent page load, if the request calls for the same file, the MDS engine will check the list, see that it already has the script, and not download/execute it a second time. Thus, the CSR code is not run and the custom rendering is not applied.

There is a way around this. SharePoint provides a function called RegisterModuleInit() which you can use to tell MDS that a particular function from a particular file should always be executed. RegisterModuleInit() takes two arguments – the path of the file that has the function to execute, and the name of the function. Now, let’s take a look at one of the popular patterns for constructing CSR template overrides, which happens to be the one I used up until this project:

  1. (function () {
  2.     var overrideCtx = {};
  3.     overrideCtx.Templates = {};
  4.     overrideCtx.Templates.Item = customItem;
  5.  
  6.     SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideCtx);
  7. })();
  8.  
  9. function customItem(ctx) {
  10.     var itemHtml = "";
  11.     // do whatever processing is necessary
  12.     // here to generate the custom html
  13.     return itemHtml;
  14. }

We can see two things here:

1. The template overrides are defined and registered with the template manager in an anonymous self-executing function, thus there is no named entry point to register with RegisterModuleInit(). In order to use RegisterModuleInit() to get the code functioning properly with MDS, the template override code needs to be changed to have a named entry point.

2. The customItem() function is defined in the global namespace. Keep in mind, for this project I was working with several template override files being loaded on the same page, many with multiple rendering override functions defined. If not handled correctly, the global namespace was going to get polluted.

So, in order to fix both of these problems, I came up with the following pattern, which I now use for all of my CSR template override scripts:

  1. var DEC = DEC || {};
  2.  
  3. DEC.thisOverride = (function () {
  4.     function customItem (ctx) {
  5.         var itemHtml = "";
  6.         // do whatever processing is necessary
  7.         // here to generate the custom html
  8.         return itemHtml;
  9.     }
  10.  
  11.     return {
  12.         render: function () {
  13.             SPClientTemplates.TemplateManager.RegisterTemplateOverrides({
  14.                 Templates: {
  15.                     Item: customItem
  16.                 }
  17.             });
  18.         }
  19.     }
  20. })();
  21.  
  22. // register for MDS enabled sites
  23. RegisterModuleInit(SPClientTemplates.Utility.ReplaceUrlTokens("~site/SiteAssets/Scripts/myOverrideScript.js"), DEC.thisOverride.render);
  24.  
  25. // fallback for non-MDS enabled sites
  26. DEC.thisOverride.render();

Let’s look at what’s going on in the code.

First, I register (or retrieve) my own namespace, so I can stay organized (line 1).

Second, I add a property thisOverride (line 3). I named it generically for this example, but the name of this property should reflect the specific SharePoint asset you are applying the override to. That way many overrides can co-exist on the same page within the custom namespace (e.g. DEC.calendarOverride, DEC.contactsOverride, DEC.customFieldXOverride, DEC.customListYOverride).

I then use the revealing module pattern to create a closure in which I contain all the functions I might need for the custom rendering. By keeping those functions private, I can still use the same internal naming convention for the actual override functions (e.g. customItem, render, etc.).  The only publicly exposed member is the render function which registers the overrides with the template manager, and becomes the named entry point.

Lastly, I register the entry point function with the MDS system (line 23), and provide a fallback for non-MDS enabled sites (line 26), ensuring that the template overrides will get applied no matter what.

Notice also that the SPClientTemplates.Utility namespace offers a function to replace URL tokens, so you can use tokens when defining the path to your override file. This allows for much greater flexibility when including the CSR script as part of a feature that may be activated on a number of sites.

So there you have it – a CSR template override pattern that is flexible enough to work on MDS enabled or disabled sites, while maintaining responsible use of the global namespace.

Friday, June 19, 2015

Hiding form fields using CSR without jQuery

A recent question on SharePoint.SE about the best way to hide fields on forms got me thinking.  Many people know about how to hide fields on forms using jQuery, by using some selector that gets you to the field's label on the form, and then hiding the containing <tr> to hide both the label and the field's control:

$('.ms-formtable nobr:contains("Field Display Name")').closest('tr').hide();

And this article shows how to apply that same jQuery technique using client side rendering / template overrides.  But I wondered, is there a way to hide fields on forms using just what you have available during CSR code execution?  Is there a way to do it without needing jQuery?

Indeed there is:

(function () {

    var overrideCtx = {};
    overrideCtx.Templates = {};
    overrideCtx.Templates.Fields = {
        "Field1": {
            "NewForm": hideField,
            "EditForm": hideField
        },
        "Field2": {
            "NewForm": hideField,
            "EditForm": hideField
        }
    };

    SPClientTemplates.TemplateManager.RegisterTemplateOverrides(overrideCtx);
})();

// the hideField function uses ctx.CurrentFieldSchema.Name,  
// so it is generic enough to be used for multiple fields

function hideField(ctx) {

    // get the span that represents the field's actual edit control
    var span = $get(ctx.FormUniqueId + ctx.FormContext.listAttributes.Id + ctx.CurrentFieldSchema.Name);

    // still need to go up two levels to hide the entire <tr>
    span.parentNode.parentNode.setAttribute("style", "display:none");
    return "";
}

I guess it just goes to show you how much you can actually do with CSR.