Thread view for SharePoint classic Discussion Board

One of the classic SharePoint list types is the Discussion Board. It allows the creation of discussion topics that users can reply to. And users can also reply to previous replies, creating a thread-like scenario.
Unfortunately, Discussion Boards currently only have a Flat view that lists all the replies ordered by creation date. This creates a very poor user experience as it’s nearly impossible to follow the replies to a given topic. Thread view used to be an available feature for discussion lists, but this is no longer the case. There is also a user voice request to bring the feature back.

But wait no more. You can find below a relatively small piece of JavaScript code that will transform the Flat view into a Thread view for discussions by overriding some out-of-the-box functions. The end result will look similar to the image below.

Thread view

The following code overrides some functions that are used by the OOB JS Link file used to generate the user interface. You can find the OOB file under /_layouts/15/sp.ui.discussions.js or /_layouts/15/sp.ui.discussions.debug.js for the debug (“readable”) version.
The code below overrides the OOB functions with a copy of that code that then contains small changes in order to apply the required modifications. I explain all the relevant code changes below, so keep reading.

var indentWidth = 30;

SP.UI.Discussions.ForumRenderer.prototype.renderForumBody = function SP_UI_Discussions_ForumRenderer$renderForumBody(context) {
    var $v_0 = new SP.HtmlBuilder();
    var $v_1 = context.forumViewState;

    if (!SP.UI.Discussions.Helpers.isNullOrUndefined($v_1)) {
        // this is where the collection of data is instantiated
        var $v_2 = context.ListData['Row'];
        var $v_3 = 0;

        if (!SP.UI.Discussions.Helpers.$Q(context) && $v_2.length > 0) {
            $v_0.addCommunitiesCssClass('postList');
            $v_0.renderBeginTag('ul');
            context.CurrentItem = $v_2[0];
            $v_0.write(this.renderForumPost(context));
            $v_0.renderEndTag();
            $v_3++;
        }
        if (!$v_2.length) {
            var $v_4 = new Array(0);

            RenderEmptyText($v_4, context);
            $v_0.write($v_4.join(''));
        }
        else {
            $v_0.addCommunitiesCssClass('postList');
            $v_0.addAttribute('id', SP.UI.Discussions.Helpers.$N($v_1.$E_0, this.get_postListId()));
            $v_0.renderBeginTag('ul');
            if ($v_3 < $v_2.length) {
                
                // this is where replies are being rendered
                console.log('Rendering custom thread view');

                // sort array by parent item - this is very important as we are assuming that the items are ordered when rendering
                $v_2.sort(function(a, b) {
                    return a['ParentItemID'] - b['ParentItemID'];
                });

                // render child items
                var childItems = this.getChildItemReplies(context, $v_2, 0, 0);
                $v_0.$0_0.push(childItems);
            }
            $v_0.renderEndTag();
        }
    }
    return $v_0.toString();
}

SP.UI.Discussions.ForumRenderer.prototype.getChildItemReplies = function getChildItemReplies(context, items, parentIndex, indentLevel) {
    var htmlBuilder = new SP.HtmlBuilder();
    var firstChildIndex = -1;

    // find index of first child
    for(var i = parentIndex + 1; i < items.length; i++) {
        if(items[i]['ParentItemID'] == items[parentIndex]['ID']) {
            firstChildIndex = i;
            break;
        }            
    }

    if(firstChildIndex > 0) {
        for(var i = firstChildIndex; i < items.length; i++) {
            // if item is a child
            if(items[i]['ParentItemID'] == items[parentIndex]['ID']) {
                context.CurrentItem = items[i];
                context.CurrentItem.IndentLevel = indentLevel;
                
                htmlBuilder.write(this.renderForumPost(context));

                // recursively call the function again to search for child items of current item
                var childItems = this.getChildItemReplies(context, items, i, indentLevel + 1);
                htmlBuilder.$0_0.push(childItems);
                
                context.CurrentItem = null;
            }
            else {
                // stop loop when item being parsed is not direct child
                break;
            }
        }
    }

    return htmlBuilder.toString();
}

SP.UI.Discussions.PostBehavior.prototype.$2C_0 = function SP_UI_Discussions_PostBehavior$$2C_0($p0, $p1) {
    this.$0_0.CurrentItem = this.jsonItem;
    if ($p1.get_$1B_0()) {
        $p0.addCommunitiesCssClass('postListItem');
        if (this.$3_0) {
            $p0.addCommunitiesCssClass('postRootContainer');
        }
        else {
            $p0.addCommunitiesCssClass('postReplyListItem');
            // indent items based on IndentLevel property
            if(this.jsonItem.hasOwnProperty('IndentLevel')) {
                $p0.addAttribute('style', 'margin-left: ' + this.jsonItem['IndentLevel'] * indentWidth + 'px; ');
            }
        }
        $p0.addAttribute('id', this.getPostElementID('Root'));
        $p0.renderBeginTag('li');
    }
    if ($p1.get_$21_0()) {
        $p0.addCommunitiesCssClass('postMainContainer');
        $p0.renderBeginTag('div');
    }
    if (this.$3_0) {
        this.renderRootPostMetaData($p0);
        $p0.addCommunitiesCssClass(this.get_$G_0() ? 'threadSubjectContainerCollapsed' : 'threadSubjectContainer');
        $p0.addAttribute('role', 'heading');
        $p0.addAttribute('aria-level', '2');
        $p0.renderBeginTag('div');
        this.$2D_0($p0, false, $p1);
        if ($p1.get_$3M_0()) {
            $p0.addCssClass('ms-floatRight');
            $p0.addCommunitiesCssClass('postExpander');
            $p0.addAttribute('id', this.getPostElementID('Expander'));
            $p0.addAttribute('title', this.get_$G_0() ? Strings.STS.L_SPDiscExpandPostAltText : Strings.STS.L_SPDiscCollapsePostAltText);
            $p0.addAttribute('href', 'javascript:;');
            $p0.renderBeginTag('a');
            $p0.addCssClass(this.get_$G_0() ? 'ms-comm-postExpanderExpandContainer' : 'ms-comm-postExpanderCollapseContainer');
            $p0.renderBeginTag('span');
            $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
            $p0.addCssClass(this.get_$G_0() ? 'ms-comm-postExpanderExpand' : 'ms-comm-postExpanderCollapse');
            $p0.renderBeginTag('img');
            $p0.renderEndTag();
            $p0.renderEndTag();
            $p0.renderEndTag();
        }
        this.$2B_0($p0, 'ms-metadata ms-comm-statsInlineContainer', 'ms-comm-statsInline', 'ms-comm-reputationNumbers');
        $p0.renderEndTag();
    }
    if (!this.get_$G_0()) {
        if (this.$3_0) {
            $p0.addCssClass('ms-comm-rootBestBackground');
            $p0.addCssClass('ms-comm-rootPostContainer');
            $p0.renderBeginTag('div');
        }
        $p0.addCssClass('ms-table');
        $p0.renderBeginTag('div');
        $p0.addCssClass('ms-tableCell');
        $p0.renderBeginTag('div');
        this.$3A_0($p0);
        $p0.renderEndTag();
        $p0.addCssClass('ms-verticalAlignTop');
        $p0.addCssClass('ms-tableCell');
        $p0.addCssClass('ms-fullWidth');
        $p0.renderBeginTag('div');
        this.$3B_0($p0);
        this.renderAuthorCardMetaData($p0);
        if ($p1.get_$3J_0()) {
            this.$3I_0($p0);
        }
        $p0.addAttribute('class', 'ms-core-defaultFont');
        $p0.renderBeginTag('div');
        this.$27_0($p0, false, $p1);
        this.$3H_0($p0);
        if ($p1.get_$29_0()) {
            this.$29_0($p0);
        }
        $p0.renderEndTag();
        $p0.renderBeginTag('div');
        $p0.addCommunitiesCssClass('postReplyContainer');
        $p0.addAttribute('id', this.getPostElementID('ReplyContainer'));
        $p0.renderBeginTag('div');
        $p0.renderEndTag();
        $p0.renderEndTag();
        $p0.renderEndTag();
        $p0.renderEndTag();
        if ($p1.get_$1j_0() && this.get_bestResponseIsVisible()) {
            $p0.addCssClass('ms-comm-bestResponseDividerHr');
            $p0.renderBeginTag('hr');
            $p0.renderEndTag();
            $p0.addCommunitiesCssClass('bestResponseContainer');
            $p0.addCommunitiesCssClass('postMainContainer');
            $p0.addAttribute('id', this.getBestResponsePostElementID());
            $p0.renderBeginTag('div');
            $p0.addCssClass('ms-textLarge');
            $p0.addCommunitiesCssClass('bestPostHeader');
            $p0.renderBeginTag('div');
            $p0.addCommunitiesCssClass('bestResponseIcon-span');
            $p0.renderBeginTag('span');
            $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
            $p0.addCommunitiesCssClass('bestResponseIcon');
            $p0.renderBeginTag('img');
            $p0.renderEndTag();
            $p0.renderEndTag();
            $p0.writeEncoded(Strings.STS.L_SPDiscBestHeader);
            $p0.renderEndTag();
            $p0.renderEndTag();
        }
        if (this.$3_0) {
            $p0.renderEndTag();
        }
    }
    if ($p1.get_$21_0()) {
        $p0.renderEndTag();
    }
    if ($p1.get_$1B_0()) {
        $p0.renderEndTag();
        if (this.$3_0) {
            $p0.addCssClass('ms-comm-allRepliesHeader');
            $p0.addAttribute('role', 'heading');
            $p0.addAttribute('aria-level', '3');
            $p0.renderBeginTag('li');
            if (this.repliesSortPicker) {
                var $v_0 = new SP.HtmlBuilder();

                $v_0.addCssClass('ms-textLarge');
                $v_0.renderBeginTag('span');
                $v_0.writeEncoded(Strings.STS.L_SPDiscAllRepliesLabel);
                $v_0.renderEndTag();
                $p0.addCommunitiesCssClass('replyHeader');
                $p0.renderBeginTag('div');
                $p0.write($v_0.toString());
                $p0.renderEndTag();
                this.repliesSortPicker.render($p0);
            }
            $p0.renderEndTag();
        }
    }
}

// required for JS overrides to render correctly 
sp_ui_discussions_initialize();

Sorting

We start by copying the content of the renderForumBody function from the
sp.ui.discussions.debug.js file and modifying it as required.

The magic starts at line 36. A simple sorting function sorts the array of items by ParentItemId. This is the property that contains the parent item for each reply. It allows us to order the data in “segments” and ensure that the child items of a given parent are positioned together within the array.

Getting child items

Next, at line 41, we call the getChildItemReplies function that returns the HTML code for the child elements of the discussion topic (item with index zero on the array) and adds that result to the HTML Builder object.

The getChildItemReplies is a function I created (is not an override, like the other 2 functions) that recursively renders child items of a parent item and invokes itself again to render the child items of each item being rendered. To improve performance, it contains some conditions that will limit the items being parsed and stop the code loops.

There is also something very important happening in line 67. context.CurrentItem.IndentLevel = indentLevel;
Here, we add a new property to each item that contains the indent level of that item. This is very important as it will allow us to easily change the styles in the next step.

Styles

In this example, all the OOB styles are preserved with the exception of the indentation for the Thread view.
We start by copying the $2C_0 function from the
sp.ui.discussions.debug.js file and modifying it as required.

The change here is extremely simple. at line 98, we simply apply a margin-left style based on the IndentLevel property that we added to the item previously. Alternatively, you could add a custom class and have additional custom CSS applied to it.

Initialize

The last line of code (266) is a function call from the original
sp.ui.discussions.debug.js file. I honestly don’t know for sure why this is required here, but without it, the custom code doesn’t override the OOB file so just keep it there 🙂

Deployment

The code below needs to be deployed as a JS Link file of a discussion board web part. Because we are overriding the OOB file, we also need to ensure that we keep loading that file.
Upload the custom JavaScript file into your site (for example, into the Site Assets library).
Edit the Discussion Board web part properties and add the following to the JS Link property:

For Discussion Board lists on normal Team sites

sp.ui.discussions.js|~sitecollection/SiteAssets/thread.js

For Discussion Board lists on Communication sites

sp.ui.discussions.js|~sitecollection/SiteAssets/thread.js|sp.ui.communities.js

32 Replies to “Thread view for SharePoint classic Discussion Board”

  1. Hi Joel,
    Thanks for your blog on this.
    I am following your solution, but If I have added the “sp.ui.discussions.js|~sitecollection/SiteAssets/thread.js|sp.ui.communities.js” into JS Link of Discussion board web part, I have experienced to view this error:
    TypeError: Object doesn’t support property or method ‘$Q’
    How can I figure it out?
    It would be great if you are able to let me know.

    Thanks. Young Ryu

  2. Yes, your code is working properly for SharePoint online. The error message I posted is coming from SharePoint 2019 on premise. Do you know why?

    1. Sorry I don’t. I have not tested it on SP 2019 and I don’t think I have an environment I can test at the moment. Will provide an update if I manage to get access to a SP 2019 environment.
      I did some additional testing after your last message, but was unable to find any issues on SP Online.

      1. Thanks. Your code is awesome for SP Online.
        Sorry. SP 2013, not SP 2019.
        How about this solution on prem?
        1) Download those libraries (sp.ui.discussions.js and sp.ui.communities.js).
        2) Rename both files and then paste them into 15 hive on the WFE server.
        3) Use the following links in JS Link:
        sp.ui.discussions_SP13.js|~sitecollection/SiteAssets/thread.js
        sp.ui.discussions_SP13.js|~sitecollection/SiteAssets/thread.js|sp.ui.communities_SP13.js
        I do not test it yet, but do you think this solution would work?

        1. There’s probably no need to copy them to the 15 hive as you can just upload all of them to a library.
          But I’m not sure it will work as it may have other script dependencies. Let me know if you try it and it works 😁

  3. If I upload the libraries file to SP library, how can I set JS Link?
    For example, sp.ui.discussions_SP13.js has been uploaded to SiteAssets.
    Correct?
    ~sitecollection/SiteAssets/sp.ui.discussions_SP13.js|~sitecollection/SiteAssets/thread.js

  4. Thank you for your quick response, but the JSLlink above is not correctly working. Only working link is “sp.ui.discussions.js|~sitecollection/SiteAssets/thread.js” in SharePoint Online.

    1. Yes that’s correct for SP online as we are loading the first file from its default location, but in SP 2013 I assumed you wanted a local copy of the first file was well.
      Let me know if your find a solution 🙂

  5. “sp.ui.discussions.js|~sitecollection/SiteAssets/Thread/sp.ui.discussions_SPOnline.js|~sitecollection/SiteAssets/Thread/Thread.js” seems working properly, but when I post a reply of some message and I got another error.
    “TypeError: Unable to get property ‘length’ of undefined or null reference”

    1. Good to know you are making progress 🙂
      Sorry I can’t really be of much help at this stage without trying it myself. I will try to get access to a SP 2013 environment to try it out, but can’t really promise anything as I currently have a lot of work going on and setting up a new environment takes some time.
      Please share updates if you find a way to make it work

  6. Hello Joel,
    Thank you for your solution! I couldn’t figure out how to connect the discussion webpart to site assets. Here is my questions.
    1. Do I have to use content editor or script editor webpart? if not where do I have to put the link?
    2. When the discussion board is in a sub site, how the URL look?

    Thank you!

    1. Hi Ray, the script reference goes on the JSLink property of the discussion web part. You can change this property editing the page/web part or using SharePoint designer.
      The believe the URL should look the same on a sub site as the script is still on the same place and we are using a token to resolve the site collection URL.
      Let me know if you have any issues trying it.

  7. Hello Joel,
    Thanks for the post
    Here is a working script for SharePoint 2016 on premise
    I hope someone will help

    ———————————————-

    var indentWidth = 30;

    SP.UI.Discussions.ForumRenderer.prototype.renderForumBody = function SP_UI_Discussions_ForumRenderer$renderForumBody(context) {
    var $v_0 = new SP.HtmlBuilder();
    var $v_1 = context.forumViewState;

    if (!SP.UI.Discussions.Helpers.isNullOrUndefined($v_1)) {
    // CPS – this is where the collection of data is instantiated
    var $v_2 = context.ListData[‘Row’];
    var $v_3 = 0;

    if (!SP.UI.Discussions.Helpers.$R(context) && $v_2.length > 0) {
    $v_0.addCommunitiesCssClass(‘postList’);
    $v_0.renderBeginTag(‘ul’);
    context.CurrentItem = $v_2[0];
    $v_0.write(this.renderForumPost(context));
    $v_0.renderEndTag();
    $v_3++;
    }
    if (!$v_2.length) {
    var $v_4 = new Array(0);

    RenderEmptyText($v_4, context);
    $v_0.write($v_4.join(”));
    }
    else {
    $v_0.addCommunitiesCssClass(‘postList’);
    $v_0.addAttribute(‘id’, SP.UI.Discussions.Helpers.$O($v_1.$E_0, this.get_postListId()));
    $v_0.renderBeginTag(‘ul’);
    if ($v_3 < $v_2.length) {

    // CPS – this is where replies are being rendered
    console.log('Rendering custom thread view');

    // CPS – sort array by parent item – this is very important as we are assuming that the items are ordered when rendering
    $v_2.sort(function(a, b) {
    return a['ParentItemID'] – b['ParentItemID'];
    });

    // CPS – render child items
    var childItems = this.getChildItemReplies(context, $v_2, 0, 0);
    $v_0.$0_0.push(childItems);
    }
    $v_0.renderEndTag();
    }
    }
    return $v_0.toString();
    }

    SP.UI.Discussions.ForumRenderer.prototype.getChildItemReplies = function getChildItemReplies(context, items, parentIndex, indentLevel) {
    var htmlBuilder = new SP.HtmlBuilder();
    var firstChildIndex = -1;

    // CPS – find index of first child
    for(var i = parentIndex + 1; i 0) {
    for(var i = firstChildIndex; i < items.length; i++) {
    // CPS – if item is a child
    if(items[i]['ParentItemID'] == items[parentIndex]['ID']) {
    context.CurrentItem = items[i];
    context.CurrentItem.IndentLevel = indentLevel;

    htmlBuilder.write(this.renderForumPost(context));

    // CPS – recursively call the function again to search for child items of current item
    var childItems = this.getChildItemReplies(context, items, i, indentLevel + 1);
    htmlBuilder.$0_0.push(childItems);

    context.CurrentItem = null;
    }
    else {
    // CPS – stop loop when item being parsed is not direct child
    break;
    }
    }
    }

    return htmlBuilder.toString();
    }

    SP.UI.Discussions.PostBehavior.prototype.$28_0 = function SP_UI_Discussions_PostBehavior$$28_0$in($p0, $p1) {
    this.$0_0.CurrentItem = this.jsonItem;
    if ($p1.$k_0) {
    $p0.addCommunitiesCssClass('postListItem');
    if (this.$3_0) {
    $p0.addCommunitiesCssClass('postRootContainer');
    }
    else {
    $p0.addCommunitiesCssClass('postReplyListItem');
    // indent items based on IndentLevel property
    if(this.jsonItem.hasOwnProperty('IndentLevel')) {
    $p0.addAttribute('style', 'margin-left: ' + this.jsonItem['IndentLevel'] * indentWidth + 'px; ');
    }
    }
    $p0.addAttribute('id', this.getPostElementID('Root'));
    $p0.renderBeginTag('li');
    }
    if ($p1.$19_0) {
    $p0.addCommunitiesCssClass('postMainContainer');
    $p0.renderBeginTag('div');
    }
    if (this.$3_0) {
    this.renderRootPostMetaData($p0);
    $p0.addCommunitiesCssClass(this.$B_0 ? 'threadSubjectContainerCollapsed' : 'threadSubjectContainer');
    $p0.addAttribute('role', 'heading');
    $p0.addAttribute('aria-level', '2');
    $p0.renderBeginTag('div');
    this.$29_0($p0, false, $p1);
    if ($p1.$1k_0) {
    $p0.addCssClass('ms-floatRight');
    $p0.addCommunitiesCssClass('postExpander');
    $p0.addAttribute('id', this.getPostElementID('Expander'));
    $p0.addAttribute('title', this.$B_0 ? Strings.STS.L_SPDiscExpandPostAltText : Strings.STS.L_SPDiscCollapsePostAltText);
    $p0.addAttribute('href', 'javascript:;');
    $p0.renderBeginTag('a');
    $p0.addCssClass(this.$B_0 ? 'ms-comm-postExpanderExpandContainer' : 'ms-comm-postExpanderCollapseContainer');
    $p0.renderBeginTag('span');
    $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
    $p0.addCssClass(this.$B_0 ? 'ms-comm-postExpanderExpand' : 'ms-comm-postExpanderCollapse');
    $p0.renderBeginTag('img');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    }
    this.$27_0($p0, 'ms-metadata ms-comm-statsInlineContainer', 'ms-comm-statsInline', 'ms-comm-reputationNumbers');
    $p0.renderEndTag();
    }
    if (!this.$B_0) {
    if (this.$3_0) {
    $p0.addCssClass('ms-comm-rootBestBackground');
    $p0.addCssClass('ms-comm-rootPostContainer');
    $p0.renderBeginTag('div');
    }
    $p0.addCssClass('ms-table');
    $p0.renderBeginTag('div');
    $p0.addCssClass('ms-tableCell');
    $p0.renderBeginTag('div');
    this.$30_0($p0);
    $p0.renderEndTag();
    $p0.addCssClass('ms-verticalAlignTop');
    $p0.addCssClass('ms-tableCell');
    $p0.addCssClass('ms-fullWidth');
    $p0.renderBeginTag('div');
    this.$31_0($p0);
    this.renderAuthorCardMetaData($p0);
    if ($p1.$1i_0) {
    this.$39_0($p0);
    }
    $p0.addAttribute('class', 'ms-core-defaultFont');
    $p0.renderBeginTag('div');
    this.$24_0($p0, false, $p1);
    this.$38_0($p0);
    if ($p1.$1h_0) {
    this.$32_0($p0);
    }
    $p0.renderEndTag();
    $p0.renderBeginTag('div');
    $p0.addCommunitiesCssClass('postReplyContainer');
    $p0.addAttribute('id', this.getPostElementID('ReplyContainer'));
    $p0.renderBeginTag('div');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    if ($p1.$1g_0 && this.get_bestResponseIsVisible()) {
    $p0.addCssClass('ms-comm-bestResponseDividerHr');
    $p0.renderBeginTag('hr');
    $p0.renderEndTag();
    $p0.addCommunitiesCssClass('bestResponseContainer');
    $p0.addCommunitiesCssClass('postMainContainer');
    $p0.addAttribute('id', this.getBestResponsePostElementID());
    $p0.renderBeginTag('div');
    $p0.addCssClass('ms-textLarge');
    $p0.addCommunitiesCssClass('bestPostHeader');
    $p0.renderBeginTag('div');
    $p0.addCommunitiesCssClass('bestResponseIcon-span');
    $p0.renderBeginTag('span');
    $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
    $p0.addCommunitiesCssClass('bestResponseIcon');
    $p0.renderBeginTag('img');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.writeEncoded(Strings.STS.L_SPDiscBestHeader);
    $p0.renderEndTag();
    $p0.renderEndTag();
    }
    if (this.$3_0) {
    $p0.renderEndTag();
    }
    }
    if ($p1.$19_0) {
    $p0.renderEndTag();
    }
    if ($p1.$k_0) {
    $p0.renderEndTag();
    if (this.$3_0) {
    $p0.addCssClass('ms-comm-allRepliesHeader');
    $p0.addAttribute('role', 'heading');
    $p0.addAttribute('aria-level', '3');
    $p0.renderBeginTag('li');
    if (this.repliesSortPicker) {
    var $v_0 = new SP.HtmlBuilder();

    $v_0.addCssClass('ms-textLarge');
    $v_0.renderBeginTag('span');
    $v_0.writeEncoded(Strings.STS.L_SPDiscAllRepliesLabel);
    $v_0.renderEndTag();
    $p0.addCommunitiesCssClass('replyHeader');
    $p0.renderBeginTag('div');
    $p0.write($v_0.toString());
    $p0.renderEndTag();
    this.repliesSortPicker.render($p0);
    }
    $p0.renderEndTag();
    }
    }
    }

    // CPS – required for JS overrides to render correctly
    sp_ui_discussions_initialize();

    1. Hi Emanuele,

      Sorry for the late reply. I just noticed that your question was still pending.
      The post was actually for SharePoint online, but I may have forgotten to mention that.

  8. Thanks Joel and Dennis – this work on SharePoint 2019 ->

    var indentWidth = 30;
    SP.UI.Discussions.ForumRenderer.prototype.renderForumBody = function SP_UI_Discussions_ForumRenderer$renderForumBody(context) {
    var $v_0 = new SP.HtmlBuilder();
    var $v_1 = context.forumViewState;
    if (!SP.UI.Discussions.Helpers.isNullOrUndefined($v_1)) {
    // CPS – this is where the collection of data is instantiated
    var $v_2 = context.ListData[‘Row’];
    var $v_3 = 0;
    if (!SP.UI.Discussions.Helpers.$R(context) && $v_2.length > 0) {
    $v_0.addCommunitiesCssClass(‘postList’);
    $v_0.renderBeginTag(‘ul’);
    context.CurrentItem = $v_2[0];
    $v_0.write(this.renderForumPost(context));
    $v_0.renderEndTag();
    $v_3++;
    }
    if (!$v_2.length) {
    var $v_4 = new Array(0);
    RenderEmptyText($v_4, context);
    $v_0.write($v_4.join(”));
    }
    else {
    $v_0.addCommunitiesCssClass(‘postList’);
    $v_0.addAttribute(‘id’, SP.UI.Discussions.Helpers.$O($v_1.$E_0, this.get_postListId()));
    $v_0.renderBeginTag(‘ul’);
    if ($v_3 < $v_2.length) {

    // CPS – this is where replies are being rendered
    console.log('Rendering custom thread view');
    // CPS – sort array by parent item – this is very important as we are assuming that the items are ordered when rendering
    $v_2.sort(function(a, b) {
    return a['ParentItemID'] – b['ParentItemID'];
    });
    // CPS – render child items
    var childItems = this.getChildItemReplies(context, $v_2, 0, 0);
    $v_0.$0_0.push(childItems);
    }
    $v_0.renderEndTag();
    }
    }
    return $v_0.toString();
    }
    SP.UI.Discussions.ForumRenderer.prototype.getChildItemReplies = function getChildItemReplies(context, items, parentIndex, indentLevel) {
    var htmlBuilder = new SP.HtmlBuilder();
    var firstChildIndex = -1;
    // CPS – find index of first child
    for(var i = parentIndex + 1; i 0) {
    for(var i = firstChildIndex; i < items.length; i++) {
    // CPS – if item is a child
    if(items[i]['ParentItemID'] == items[parentIndex]['ID']) {
    context.CurrentItem = items[i];
    context.CurrentItem.IndentLevel = indentLevel;

    htmlBuilder.write(this.renderForumPost(context));
    // CPS – recursively call the function again to search for child items of current item
    var childItems = this.getChildItemReplies(context, items, i, indentLevel + 1);
    htmlBuilder.$0_0.push(childItems);

    context.CurrentItem = null;
    }
    else {
    // CPS – stop loop when item being parsed is not direct child
    break;
    }
    }
    }
    return htmlBuilder.toString();
    }
    SP.UI.Discussions.PostBehavior.prototype.$2C_0 = function SP_UI_Discussions_PostBehavior$$2C_0($p0, $p1) {
    this.$0_0.CurrentItem = this.jsonItem;
    if ($p1.get_$1B_0()) {
    $p0.addCommunitiesCssClass('postListItem');
    if (this.$3_0) {
    $p0.addCommunitiesCssClass('postRootContainer');
    }
    else {
    $p0.addCommunitiesCssClass('postReplyListItem');
    // indent items based on IndentLevel property
    if(this.jsonItem.hasOwnProperty('IndentLevel')) {
    $p0.addAttribute('style', 'margin-left: ' + this.jsonItem['IndentLevel'] * indentWidth + 'px; ');
    }
    }
    $p0.addAttribute('id', this.getPostElementID('Root'));
    $p0.renderBeginTag('li');
    }
    if ($p1.get_$21_0()) {
    $p0.addCommunitiesCssClass('postMainContainer');
    $p0.renderBeginTag('div');
    }
    if (this.$3_0) {
    this.renderRootPostMetaData($p0);
    $p0.addCommunitiesCssClass(this.get_$G_0() ? 'threadSubjectContainerCollapsed' : 'threadSubjectContainer');
    $p0.addAttribute('role', 'heading');
    $p0.addAttribute('aria-level', '2');
    $p0.renderBeginTag('div');
    this.$2D_0($p0, false, $p1);
    if ($p1.get_$3M_0()) {
    $p0.addCssClass('ms-floatRight');
    $p0.addCommunitiesCssClass('postExpander');
    $p0.addAttribute('id', this.getPostElementID('Expander'));
    $p0.addAttribute('title', this.get_$G_0() ? Strings.STS.L_SPDiscExpandPostAltText : Strings.STS.L_SPDiscCollapsePostAltText);
    $p0.addAttribute('href', 'javascript:;');
    $p0.renderBeginTag('a');
    $p0.addCssClass(this.get_$G_0() ? 'ms-comm-postExpanderExpandContainer' : 'ms-comm-postExpanderCollapseContainer');
    $p0.renderBeginTag('span');
    $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
    $p0.addCssClass(this.get_$G_0() ? 'ms-comm-postExpanderExpand' : 'ms-comm-postExpanderCollapse');
    $p0.renderBeginTag('img');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    }
    this.$2B_0($p0, 'ms-metadata ms-comm-statsInlineContainer', 'ms-comm-statsInline', 'ms-comm-reputationNumbers');
    $p0.renderEndTag();
    }
    if (!this.get_$G_0()) {
    if (this.$3_0) {
    $p0.addCssClass('ms-comm-rootBestBackground');
    $p0.addCssClass('ms-comm-rootPostContainer');
    $p0.renderBeginTag('div');
    }
    $p0.addCssClass('ms-table');
    $p0.renderBeginTag('div');
    $p0.addCssClass('ms-tableCell');
    $p0.renderBeginTag('div');
    this.$3A_0($p0);
    $p0.renderEndTag();
    $p0.addCssClass('ms-verticalAlignTop');
    $p0.addCssClass('ms-tableCell');
    $p0.addCssClass('ms-fullWidth');
    $p0.renderBeginTag('div');
    this.$3B_0($p0);
    this.renderAuthorCardMetaData($p0);
    if ($p1.get_$3J_0()) {
    this.$3I_0($p0);
    }
    $p0.addAttribute('class', 'ms-core-defaultFont');
    $p0.renderBeginTag('div');
    this.$27_0($p0, false, $p1);
    this.$3H_0($p0);
    if ($p1.get_$29_0()) {
    this.$29_0($p0);
    }
    $p0.renderEndTag();
    $p0.renderBeginTag('div');
    $p0.addCommunitiesCssClass('postReplyContainer');
    $p0.addAttribute('id', this.getPostElementID('ReplyContainer'));
    $p0.renderBeginTag('div');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.renderEndTag();
    if ($p1.get_$1j_0() && this.get_bestResponseIsVisible()) {
    $p0.addCssClass('ms-comm-bestResponseDividerHr');
    $p0.renderBeginTag('hr');
    $p0.renderEndTag();
    $p0.addCommunitiesCssClass('bestResponseContainer');
    $p0.addCommunitiesCssClass('postMainContainer');
    $p0.addAttribute('id', this.getBestResponsePostElementID());
    $p0.renderBeginTag('div');
    $p0.addCssClass('ms-textLarge');
    $p0.addCommunitiesCssClass('bestPostHeader');
    $p0.renderBeginTag('div');
    $p0.addCommunitiesCssClass('bestResponseIcon-span');
    $p0.renderBeginTag('span');
    $p0.addAttribute('src', GetThemedImageUrl('spcommon.png'));
    $p0.addCommunitiesCssClass('bestResponseIcon');
    $p0.renderBeginTag('img');
    $p0.renderEndTag();
    $p0.renderEndTag();
    $p0.writeEncoded(Strings.STS.L_SPDiscBestHeader);
    $p0.renderEndTag();
    $p0.renderEndTag();
    }
    if (this.$3_0) {
    $p0.renderEndTag();
    }
    }
    if ($p1.get_$21_0()) {
    $p0.renderEndTag();
    }
    if ($p1.get_$1B_0()) {
    $p0.renderEndTag();
    if (this.$3_0) {
    $p0.addCssClass('ms-comm-allRepliesHeader');
    $p0.addAttribute('role', 'heading');
    $p0.addAttribute('aria-level', '3');
    $p0.renderBeginTag('li');
    if (this.repliesSortPicker) {
    var $v_0 = new SP.HtmlBuilder();
    $v_0.addCssClass('ms-textLarge');
    $v_0.renderBeginTag('span');
    $v_0.writeEncoded(Strings.STS.L_SPDiscAllRepliesLabel);
    $v_0.renderEndTag();
    $p0.addCommunitiesCssClass('replyHeader');
    $p0.renderBeginTag('div');
    $p0.write($v_0.toString());
    $p0.renderEndTag();
    this.repliesSortPicker.render($p0);
    }
    $p0.renderEndTag();
    }
    }
    }
    // CPS – required for JS overrides to render correctly
    sp_ui_discussions_initialize();

  9. Hi Joel, Thanks for writing this post. It is very helpful.

    But, Does it stop working properly when paging starts coming as discussion increase???

    1. Hi Vikas, I remember testing this with some very large discussions and paging was working well at the time. Not sure if something could have changed and introduced issues

  10. Hi Joel, When I added the threaded discussion script “Like” button disappears . How do I add that again

    1. Hi Chandra, sorry I have missed your comment previously.
      I assume Microsoft has made changes to the code since I have published it, so not sure if this would still work without creating any issues. Did you find a solution?

  11. Hi Joel,
    We have tried your solution in SharePoint 2016. Although it works fine 🙂 (thank you!)we have a little issue, when we click in the forum option we have to reload the page in order to see the nested view, otherwise the view is shown with the original flat design. I don´t know if anybody has this same issue and if there is an option to improve it.
    Thanks in advance.

    1. Hi, sorry for the late reply, had an issue with comments…
      I no longer have access to this implementation and don’t currently have an on-prem environment so won’t be able to test

    1. Hi Shahid, sorry for the late reply. I hope you have resolved the problem…I haven’t done anything with discussions for a long time, so I’m afraid I think I can’t be of much help

Leave a Reply

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