M365 Dev Blog

Thread view for SharePoint classic Discussion Board

thread view

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
Exit mobile version