Add Comments And Replies To SharePoint Online Page Layout

We were in the process of redesigning our intranet from SP2010 to SP Online. Most of the features in SP2010 were customized using farm solution. Achieving the same functionalities in SharePoint Online wasn’t easy.

One such requirement was to have the ability to comment and reply on the comments on the news articles. Obviously, there was no out-of-the-box page layout in SharePoint online that gives this feature, so we had to do something custom.

So, this blog is about how we achieved it using SharePoint REST API, knockout, and bootstrap. All the required references for knockout.js, boostrap.min.js, bootstrap.min.css were all already in the Master page.

Step 1

We created 2 lists. One for comments and another for Replies. In the comments list, we created two columns,

  • An ID column to identify to which page the comments belongs to. In our case, we used the page URL itself as the ID.
  • Comments column – to save the comments

In Replies list, we created 2 columns,

  • Comment ID – to identify to which comment the reply belongs to. This is a foreign key reference to ID column of the comments list
  • Replies column – To save the replies to the comment

Step 2

We created a custom page layout for publishing news articles. (Or, alternatively, we can make a copy of any of the existing page layout and customise it.) Edit this page layout using SharePoint designer in Advanced mode to add the required HTML and JavaScript code.

Step 3

The following HTML code was added to the page layout.

  1. <div class="container">  
  2.     <div class="form-group col-md-6">  
  3.         <hr> <label for="comment">Enter Comments:</label> <textarea class="form-control " rows="3" id="comment"></textarea> <a href="" ID="btn_CommentsSave" class="cbs-Line1Link ms-noWrap ms-displayBlock" data-bind="click: createListItem">  
  4. <span> <i class="ms-Icon ms-Icon--post"></i> Post Comment</span>  
  5. </a></div>  
  6. </div>  
  7. <div class="container">  
  8.     <div class="row">  
  9.         <div class="col-md-8">  
  10.             <h2 class="page-header"><span data-bind="text: commentsCount"></span> Comments</h2>  
  11.             <hr>  
  12.             <section class="comment-list">  
  13.                 <!-- First Comment -->  
  14.                 <div class="row" data-bind="foreach: commentsfromUsers" style="padding: 5px !important">  
  15.                     <div class="col-md-2 col-sm-2 hidden-xs" style="padding: 5px !important">  
  16.                         <figure class="thumbnail"> <img class="img-responsive" data-bind="attr:{src: photo}" />  
  17.                             <figcaption class="text-center" data-bind="text: userName"></figcaption>  
  18.                         </figure>  
  19.                     </div>  
  20.                     <div class="col-md-10 col-sm-10">  
  21.                         <div class="panel panel-default arrow left">  
  22.                             <div class="panel-body" style="border:thin #C0C0C0 solid !important;">  
  23.                                 <header class="text-left"> <a><i class="ms-Icon ms-Icon--person" aria-hidden="true"></i></a> <span data-bind="text: userName"> </span> <a><i class="ms-Icon ms-Icon--event" aria-hidden="true"></i></a> <time datetime="16-12-2014 01:05" data-bind="text: Created"></time> </header>  
  24.                                 <div class="comment-post" data-bind="text: Comments"></div>  
  25.                                 <div class="text-right" align="right"><span data-bind="text: repliesCount"></span> reply(s) <a href="#" data-bind="click: $root.ShowHideItems"><i class="ms-Icon ms-Icon--reply"></i> Reply</a></div>  
  26.                             </div>  
  27.                         </div>  
  28.                     </div>  
  29.                     <div class="row" data-bind="attr:{id: 'divReply'+Id}" style="display:none !important;padding: 5px !important;" align="right">  
  30.                         <div class="col-md-8 col-sm-8 col-md-offset-3 col-sm-offset-0 hidden-xs">  
  31.                             <div class="form-group col-md-8"> <textarea class="form-control " rows="3" data-bind="attr:{id: 'addReply'+Id}"></textarea> <a ID="btn_addReplySave" href="" data-bind="click: $root.createReplyListItem">  
  32. <i class="ms-Icon ms-Icon--post"></i> Post Reply  
  33. </a></div>  
  34.                         </div>  
  35.                     </div>  
  36.                     <div class="row"></div>  
  37.                     <!-- Second Comment Reply -->  
  38.                     <div data-bind="foreach: replies ">  
  39.                         <div class="row">  
  40.                             <div class="col-md-2 col-sm-2 col-md-offset-1 col-sm-offset-0 hidden-xs">  
  41.                                 <figure class="thumbnail"> <img class="img-responsive" data-bind="attr:{src: photo}" />  
  42.                                     <figcaption class="text-center" data-bind="text: userName"></figcaption>  
  43.                                 </figure>  
  44.                             </div>  
  45.                             <div class="col-md-8 col-sm-8">  
  46.                                 <div class="panel panel-default arrow left">  
  47.                                     <div class="panel-heading right"> Reply</div>  
  48.                                     <div class="panel-body" style="border:thin #C0C0C0 solid !important;">  
  49.                                         <header class="text-left"> <a><i class="ms-Icon ms-Icon--person" aria-hidden="true"></i></a> <span data-bind="text: userName"></span> <a><i class="ms-Icon ms-Icon--event" aria-hidden="true"></i></a> <time datetime="16-12-2014 01:05" data-bind="text: Created"> </time> </header>  
  50.                                         <div class="comment-post" data-bind="text: reply"></div>  
  51.                                     </div>  
  52.                                 </div>  
  53.                             </div>  
  54.                         </div>  
  55.                     </div>  
  56.                 </div>  
  57.             </section>  
  58.         </div>  
  59.     </div>  
  60. </div>  

Step 4

The following JavaScript was added to the page layout.

  1. var siteCollectionURL = _spPageContextInfo.webAbsoluteUrl;  
  2. var pageurl = window.location.href.replace("%20""").replace("%27""").replace(/[&$%‘’']/g, "");  
  3.   
  4. function ShowHideItems() {  
  5.     document.getElementById('replyToComments').style.display = 'block';  
  6. }  
  7. var Comment = function(comment, Id, Created, CreatedById, replies) {  
  8.     this.Comments = comment;  
  9.     this.Id = Id;  
  10.     var num = parseInt(Created.replace(/[^0-9]/g, ""));  
  11.     var date = new Date(num);  
  12.     this.Created = date.toLocaleString();  
  13.     this.CreatedById = CreatedById;  
  14.     this.replies = ko.observableArray(replies);  
  15.     this.photo = ko.observable();  
  16.     this.userName = ko.observable();  
  17.     this.repliesCount = ko.observable(replies.length);  
  18.     var self = this;  
  19.     $.ajax({  
  20.         url: _spPageContextInfo.webServerRelativeUrl + '/_vti_bin/listdata.svc/UserInformationList?$filter=Id%20eq%20' + CreatedById,  
  21.         method: "GET",  
  22.         headers: {  
  23.             "Accept""application/json; odata=verbose"  
  24.         },  
  25.         success: function(data) {  
  26.             self.photo("https://<TenantURL>/_layouts/15/userphoto.aspx?size=S&accountname=" + data.d.results[0].WorkEmail.toString());  
  27.             self.userName(data.d.results[0].Name);  
  28.         },  
  29.         error: function(data) {  
  30.             alert('Error' + data.error);  
  31.         }  
  32.     });  
  33. }  
  34. var Reply = function(reply, Id, ParentId, Created, CreatedById) {  
  35.     this.reply = reply;  
  36.     this.Id = Id;  
  37.     this.ParentId = ParentId;  
  38.     var num = parseInt(Created.replace(/[^0-9]/g, ""));  
  39.     var date = new Date(num);  
  40.     this.Created = date.toLocaleString();  
  41.     this.CreatedById = CreatedById;  
  42.     this.photo = ko.observable();  
  43.     this.userName = ko.observable();  
  44.     var self = this;  
  45.     $.ajax({  
  46.         url: _spPageContextInfo.webServerRelativeUrl + '/_vti_bin/listdata.svc/UserInformationList?$filter=Id%20eq%20' + CreatedById,  
  47.         method: "GET",  
  48.         headers: {  
  49.             "Accept""application/json; odata=verbose"  
  50.         },  
  51.         success: function(data) {  
  52.             self.photo("https://<TenantURL>/_layouts/15/userphoto.aspx?size=S&accountname=" + data.d.results[0].WorkEmail.toString());  
  53.             self.userName(data.d.results[0].Name);  
  54.         },  
  55.         error: function(data) {  
  56.             alert('Error' + data.error);  
  57.         }  
  58.     });  
  59. }  
  60.   
  61. function getReplies(ID) {  
  62.     var commentsurl = siteCollectionURL + '/_vti_bin/listdata.svc/Replies?$filter=%20ParentID%20eq%20%27' + ID + '%27';  
  63.     var replies = new Array();  
  64.     $.ajax({  
  65.         url: siteCollectionURL + '/_vti_bin/listdata.svc/Replies?$filter=%20ParentID%20eq%20%27' + ID + '%27',  
  66.         method: "GET",  
  67.         headers: {  
  68.             "Accept""application/json; odata=verbose"  
  69.         },  
  70.         success: function(data2) {  
  71.             for (var i = 0; i < data2.d.results.length; i++) {  
  72.                 replies.push(new Reply(data2.d.results[i].Reply, data2.d.results[i].Id, ID, data2.d.results[i].Created, data2.d.results[i].CreatedById));  
  73.             }  
  74.         },  
  75.         error: function(data2) {  
  76.             replies = null;  
  77.         },  
  78.         async: false  
  79.     });  
  80.     return replies;  
  81. }  
  82.   
  83. function ClearFields() {  
  84.     document.getElementById('comment').value = "";  
  85. }  
  86.   
  87. function getParameterByName(name, url) {  
  88.     if (!url) url = window.location.href;  
  89.     name = name.replace(/[\[\]]/g, "\\$&");  
  90.     var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),  
  91.         results = regex.exec(url);  
  92.     if (!results) return null;  
  93.     if (!results[2]) return '';  
  94.     return decodeURIComponent(results[2].replace(/\+/g, " "));  
  95. }  
  96.   
  97. function CommentReplyViewModel() {  
  98.     var self = this;  
  99.     self.userID = ko.observable();  
  100.     self.commentsfromUsers = ko.observableArray();  
  101.     self.RepliesfromUsers = ko.observableArray();  
  102.     self.commentsCount = ko.observable();  
  103.     self.ShowHideItems = function(data) {  
  104.         $('#divReply' + data.Id).slideToggle();  
  105.     }  
  106.     self.getCommentsListItem = function() {  
  107.         var commentsurl = siteCollectionURL + '/_vti_bin/listdata.svc/Comments?$filter=%20Title%20eq%20%27' + pageurl + '%27';  
  108.         $.ajax({  
  109.             url: siteCollectionURL + '/_vti_bin/listdata.svc/Comments?$filter=%20Title%20eq%20%27' + pageurl + '%27',  
  110.             method: "GET",  
  111.             headers: {  
  112.                 "Accept""application/json; odata=verbose"  
  113.             },  
  114.             success: function(data2) {  
  115.                 self.commentsfromUsers.removeAll();  
  116.                 self.commentsCount(data2.d.results.length);  
  117.                 for (var i = 0; i < span data - mce - type = "bookmark"  
  118.                     id = "mce_SELREST_start"  
  119.                     data - mce - style = "overflow:hidden;line-height:0"  
  120.                     style = "overflow:hidden;line-height:0" > < /span><span data-mce-type="bookmark" id="mce_SELREST_start" data-mce-style="overflow:hidden;line-height:0" style="overflow:hidden;line-height:0" ></span > < data2.d.results.length; i++) {  
  121.                     self.commentsfromUsers.push(new Comment(data2.d.results[i].Comments, data2.d.results[i].Id, data2.d.results[i].Created, data2.d.results[i].CreatedById, getReplies(data2.d.results[i].Id)));  
  122.                 }  
  123.             },  
  124.             error: function(data2) {  
  125.                 alert('Error' + data.error);  
  126.             }  
  127.         });  
  128.     }  
  129.     self.createListItem = function() {  
  130.         var commentsValue = document.getElementById('comment').value;  
  131.         var url = siteCollectionURL + "/_vti_bin/ListData.svc/Comments";  
  132.         var Comments = {  
  133.             Comments: commentsValue,  
  134.             SiteURL: pageurl,  
  135.             Title: pageurl  
  136.         };  
  137.         var body = JSON.stringify(Comments);  
  138.         $.ajax({  
  139.             type: "POST",  
  140.             url: url,  
  141.             contentType: 'application/json',  
  142.             processData: false,  
  143.             data: body,  
  144.             success: function() {  
  145.                 alert('Comments Saved Successfully.');  
  146.                 document.getElementById('comment').value = "";  
  147.                 self.getCommentsListItem();  
  148.             }  
  149.         });  
  150.     }  
  151.     self.createReplyListItem = function(data) {  
  152.         var RepliesValue = document.getElementById('addReply' + data.Id).value;  
  153.         var pageurl = window.location.href;  
  154.         var allReplies;  
  155.         var url = siteCollectionURL + "/_vti_bin/ListData.svc/Replies";  
  156.         var Replies = {  
  157.             Reply: RepliesValue,  
  158.             ParentID: data.Id.toString(),  
  159.             Title: pageurl  
  160.         };  
  161.         var body = JSON.stringify(Replies);  
  162.         $.ajax({  
  163.             type: "POST",  
  164.             url: url,  
  165.             contentType: 'application/json',  
  166.             processData: false,  
  167.             data: body,  
  168.             success: function() {  
  169.                 alert('Reply Saved Successfully.');  
  170.                 document.getElementById('addReply' + data.Id).value = "";  
  171.                 $('#divReply' + data.Id).slideToggle();  
  172.                 allReplies = getReplies(data.Id);  
  173.                 data.replies(allReplies);  
  174.                 data.repliesCount(allReplies.length);  
  175.             },  
  176.             error: function(error) {  
  177.                 alert(error.error);  
  178.             },  
  179.             async: false  
  180.         });  
  181.     }  
  182.     $.ajax({  
  183.         url: url,  
  184.         method: "GET",  
  185.         headers: {  
  186.             "Accept""application/json; odata=verbose"  
  187.         },  
  188.         success: function(data) {  
  189.             self.getCommentsListItem();  
  190.         },  
  191.         error: function(data) {  
  192.             alert(data.error);  
  193.         }  
  194.     });  
  195. }  
  196. $(document).ready(function() {  
  197.     ko.applyBindings(new CommentReplyViewModel());  
  198. });  

 

Step 5

Check-In and publish the page layout as a major version.

Step 6

Create a SharePoint page using the new page layout and you will see the following at the bottom of the article (if that is where you have placed the HTML)

Comments

In case if there are comments and replies, you will see –

Replies

As you can see, it shows the photo of the user who has commented and replied and also it will show “number of replies” for a comment. When you click on “Reply”, the reply comment box would appear.

Hope this is useful for someone.