The only decent blogs mentioning CSR and Grouping are:

But neither solve the issue I have

CSR code also affects the Grouping display

I have CSR code which displays a standard Choice Column 'ratingBusiness' with options 0,1,2,3,4,5 as:

Users can change the ratingBusiness Number Value straight from the List View.
As you can see, Issue is that same CSR code is also applied to the Group By header.

Adding grouping with CSR

I can overrule the CSR Grouping with

FieldContext.Templates.Group = 
  function(ctx, group, groupId, listItem, listSchema, level, expand){
    return '<div style="font-weight:bold">' + listItem[group] + '</div>';

But then more issues occur:

  • Now all Group By ratingBusiness Number values are displayed at the top of the list
  • all UX to collapse/uncollapse is gone.

Is it possible at all to retain the standard Number display in the Grouping headers?

The CSR function to display ratingBusiness in the Group By header is called once (with the first List Item as data), but at first sight I see no indication whether the code was called to display the item code or the Group By header code.


I now created a Calculated Column referencing rateBusiness and use that to Group By. This gets me the (standard) Group By I am after.

But.. I have plenty more rating Columns, so then need an extra Calculated Column for every rating Column :-/



Update #1

If I set Group By COLLAPSED:

The ctx.CurrentItem object will not contain the CurrentItem data but only the groupheading info:

    firstRow: true,
    ratingBusiness: "5",
    ratingBusiness.COUNT.group: "53",
    ratingBusiness.groupindex: "1_",
    ratingBusiness.newgroup: "1",
    ratingBusiness.urlencoded: "%3B%235%3B%23"

So I can 'catch' the display in the Group heading with:

    if (!CurrentItem.hasOwnProperty('ID')) { //not a ListItem
        return CurrentItem[CurrentFieldSchema.Name];

Set to Group By EXPAND and ctx.CurrentItem will always contain the first CurrentItem

2 Answers 2


After some intense debugging I am answering my own question.
And wasted +50 StackOverflow points on this, so am an idiot.

I copied and de-minified the original (global) SharePoint RenderGroupTemplate function

The relevant lines:

    delete b[d+".groupHeader"];


    listItem[group + ".groupHeader"] = true;
    a += spMgr.RenderFieldByName(ctx, group, listItem, listSchema);
    delete listItem[group + ".groupHeader"];

So SharePoint actually does assign an indication of rendering as group header.
But deletes this [fieldname] .groupHeader property as well.

Objects in JavaScript are always passed by reference;
That is why I never saw this property in the Developer Console.

All I have to do now is change the previous (but partial) workaround

    if (!CurrentItem.hasOwnProperty('ID')) { //not ListItem
        return CurrentItem[CurrentFieldSchema.Name];


    if (CurrentItem.hasOwnProperty(CurrentFieldSchema.Name + '.groupHeader')) {//is groupHeader
        return CurrentItem[CurrentFieldSchema.Name];

For completeness the full de-minified version of the standard SharePoint RenderGroupTemplate function

Note the above applies because I used a Choice field; the RenderFieldByName function is not called for other fieldtypes.

window.RenderGroupTemplate = function (ctx, group, groupId, listItem, listSchema, level, collapse) { 
    /*global ListView,DOM,GetThemedImageUrl,Encoding,spMgr,RenderAggregate*/
    ctx.CurrentItem = listItem;
    var q = ctx.ctxId,
        a = '<tbody id="titl';
    a += groupId + '" groupString="' + listItem[group + ".urlencoded"] + '"';
    if (level == 2 && !collapse) a += ' style="display:none"';
    a += '><tr><td colspan="100" nowrap="nowrap" class="ms-gb';
    if (level == 2) a += "2";
    a += '">';
    if (level == 2) a += "<img src=" + ListView.ImageBasePath + '"/_layouts/15/images/blank.gif?rev=40" alt="" height="1" width="10">';
    a += '<a href="javascript:" onclick="javascript:ExpCollGroup(';
    a += "'" + groupId + "', 'img_" + groupId + "',event, false);return false;" + '">';
    var l = null,
        m = null;
    if (DOM.rightToLeft) {
        l = collapse ? "ms-commentcollapsertl-iconouter" : "ms-commentexpandrtl-iconouter";
        m = collapse ? "ms-commentcollapsertl-icon" : "ms-commentexpandrtl-icon";
    } else {
        l = collapse ? "ms-commentcollapse-iconouter" : "ms-commentexpand-iconouter";
        m = collapse ? "ms-commentcollapse-icon" : "ms-commentexpand-icon";
    var p = collapse ? window.ListView.Strings.L_SPCollapse : window.ListView.Strings.L_SPExpand;
    a += '<span class="' + l + '"><img class="' + m + '" src="' + GetThemedImageUrl("spcommon.png");
    a += '" alt="' + p + '" id="img_' + groupId + '" /></span>';
    var fieldSchema;
    for (var groupDisplayName = group, groupfieldSchema, k = 0; k < listSchema.Field.length; k++) {
        fieldSchema = listSchema.Field[k];
        if (fieldSchema.Name == group) {
            groupDisplayName = fieldSchema.DisplayName;
            groupfieldSchema = fieldSchema;
    a += Encoding.HtmlEncode(groupDisplayName) + "</a> : ";
    if (groupfieldSchema !== null) {
        var fType = groupfieldSchema.Type;
        if (fType == "Number" || fType == "Currency") {
            a += listItem[fieldSchema.Name];
        } else if (fType == "DateTime" && Boolean(listItem[fieldSchema.Name + ".groupdisp"])) {
            a += listItem[fieldSchema.Name + ".groupdisp"];
        } else if (fType == "User" || fType == "UserMulti") {
            a += listItem[fieldSchema.Name + ".span"];
        } else {
            ctx.CurrentItemIdx = k;
            listItem[group + ".groupHeader"] = true;
            a += spMgr.RenderFieldByName(ctx, group, listItem, listSchema);
            delete listItem[group + ".groupHeader"];
            ctx.CurrentItemIdx = -1;
    a += ' <span style="font-weight: lighter; display: inline-block;">(';
    a += level == 2 ? listItem[group + ".COUNT.group2"] : listItem[group + ".COUNT.group"];
    a += ")</span></td></tr></tbody>";
    var o = listSchema.Aggregate;
    if (o !== null && !ctx.inGridMode) {
        a += RenderAggregate(ctx, groupId, listItem, listSchema, level, collapse, o);
    ctx.CurrentItem = null;
    return a;



A possible solution that works with both expanded and collapsed groups (one or two groups) is hooking up to the group rendering and extending the field with the information that it is about to be rendered as a group header:

Group: function(renderCtx, group, groupId, listItem, listSchema, level, expand){
    listItem[group + '.groupHeader'] = true;
    return RenderGroupTemplate(renderCtx, group, groupId, listItem, listSchema, level, expand);

Remember to reset the groupHeader property so it is rendered normally the next time.

'MyField': {
    View: function(ctx, field, listItem, listSchema){   
        if (listItem[field.RealFieldName + '.groupHeader']){
            listItem[field.RealFieldName + '.groupHeader'] = false;
            return renderFieldAsGroupHeader(...);
        } else {
            return renderField(...)


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.