Using spservices to create discussion and reply in a Discussion List

This updated blog post describes how I go about building a threaded, inline comments system for any page, using a SharePoint Discussion List as the backend storage for threaded data.

I choose to use SPServices because originally, the webpart is built for a SharePoint 2007 environment.  

I've rebuilt the webpart using the JavaScript code and confirm it runs happily in SharePoint 2013, and this time, with much better screenshots!
In SharePoint 2010 and 2013, you should be able to talk directly to the REST endpoint to perform read and updates.

Enter the Discussion List

 

Many users have a love-hate relationship with SharePoint's Discussion list.  Perhaps more hate.  The main culprit is the inflexible UI.  Lack of many modern forum needs:

  • Can't show all discussions and show threaded replies together
  • Can't do inline new discussion
  • Can't do inline replies
  • Poor support cross browsers
  • Can't easily filter for a subset

But still, the list itself is very functional.

  • Does support threaded discussions
  • Calculates total replies and last discussion updated
  • Has email notification through SharePoint alerts
  • Has tweaked permission model where users can create but not modify their own posts, or can edit.

 

Image888

Figure: A basic SharePoint Discussion List

 

Let's ignore the problems with the native Discussion List UI, and build our own with JavaScript

Read from the Discussion List

 

/*
This code queries using SPServices' GetListItems on list and populates a UL.discussions dom element on the page
*/
var $ul = $("ul.discussions");
var list = "My Discussions";

function getDiscussionList(list, $ul) {
var promise = $().SPServices({
  operation: "GetListItems",
  listName: list,
  CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Body' /><FieldRef Name='Author' /><FieldRef Name='Modified' /><FieldRef Name='DiscussionLastUpdated' /><FieldRef Name='ItemChildCount' /></ViewFields>",
  CAMLQuery: "<Query><OrderBy><FieldRef  Name='DiscussionLastUpdated' Ascending='FALSE' /></OrderBy></Query>"
});

promise.done(function(){
  var items = $(promise.responseXML).SPFilterNode("z:row").SPXmlToJson({
    mapping: {},
    includeAllAttrs: true,
    removeOws: true
  });
  $ul.empty(); // clear old list

  $.each(items, function(index, item) {
    // render each discussion
    var $li = $("<li class=discussion' />");
    $li.append(item.Title);
    $li.append(item.Body);
    $li.append(item.Author);
    $li.append(item.Modified);
    $li.append(item.DiscussionLastUpdated);
    $li.append(item.ItemChildCount);
    $li.append(item.FileRef);
    $li.append("<div class='replies'>replies</div>");
    $ul.append($li);

  });  // end items.each
}); // end promise done
promise.fail(function(xhr, status, error) {
  alert(xhr.responseText);
}); // end promise fail

} // end getDiscussionList


Key points:

  • List.asmx - GetListItems
  • This returns the top level items in the list, which are Discussions

 

Figure out the threaded discussions


In SharePoint discussion lists, the top level "Discussion" items are folders.  The subsequent "Message" reply items are list items within that folder.
So to query for the replies to a discussion, we query the list with the filepath of the top level Discussion item as the query filter.

/*
This second function figure out the threaded replies when you expand one discussion
*/

Note: The FileRef usually has a value that looks like this:
// 15;#Company/Site1/Web1/Lists/My Discussions/15_.000
You need to clean the URL and use only the file path:

function getFilePath(fileRef) {
  if (!fileRef) return;
  var m = /;#(.*)$/.exec(fileRef);
  if (m) {
    return m[1];
  }
}

This will give you:
Company/Site1/Web1/Lists/My Discussions/15_.000

function getDiscussionReplies(list, filepath) {
var options = {
  operation: "GetListItems",
  listName: list,
  CAMLViewFields: "<ViewFields><FieldRef Name='Title' /><FieldRef Name='Body' /><FieldRef Name='Author' /><FieldRef Name='Modified' /><FieldRef Name='DiscussionLastUpdated' /><FieldRef Name='ItemChildCount' /></ViewFields>",
  CAMLQueryOptions: "<QueryOptions><ViewAttributes Scope='RecursiveAll' IncludeRootFolder='True' /></QueryOptions>",
  CAMLQuery: "<Query><Where><Contains><FieldRef Name='FileRef' /><Value Type='Text'>" + filepath + "</Value></Contains></Where><OrderBy><FieldRef  Name='FileRef' Ascending='TRUE' /></OrderBy></Query>"
};
var promise = $().SPServices(options);

promise.done(function(){
  var items = $(promise.responseXML).SPFilterNode("z:row").SPXmlToJson({
    mapping: {},
    includeAllAttrs: true,
    removeOws: true
  });

  $.each(items, function(index, item) {
    // render each reply
    // snipped

  });  // end items.each
}); // end promise done

} // end getDiscussionReplies

 

Key points:

  • Use CAMLQueryOptions for RecursiveAll
  • Use FileRef contains FilePath to find all threaded (Message) replies

Create a Discussion in the Discussion List

To create a new discussion, you just need to create a new item in the Discussion List

var list = "My Discussions";
var title = $("input.title").text();
var body = $("textarea.body").text();

function newDiscussion(list, title, body) {

var promise = $().SPServices({
  operation: "UpdateListItems",
  batchCmd: "New",
  listName: "Team Discussion",
  updates: "<Batch OnError='Continue' >" +
       "<Method ID='1' Cmd='New'>" +
        "<Field Name='ContentType'>Discussion</Field>" +
       "<Field Name='FSObjType'>1</Field>" +   // Important: FSObjType = 1 means that this is a folder.  If this isn't specified SharePoint sometimes create the wrong root level item.
        "<Field Name='Title'>" + escapeColumnValue(title) + "</Field>" +
        "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
       "</Method>" +
      "</Batch>"
});
   
promise.done(function(){
  var $ul = $("ul.discussions");
  getDiscussionList(list, $ul);

}); // end promise done

}

 

Image893

Figure: Start a new discussion inline

Key points:

  • List.asmx - UpdateListItems
  • Set FSObjType = 1, this is necessary or there were head-scratching bugs

 

(Optional) Filter a Discussion List to only related item

 

In the Discussion List settings, you can add a lookup column RelatedItem to an external List (or Document Library).
Then when you use the JavaScript above, you can filter your CAML Queries so that they only return elements that are related to the current page.

For getDiscussionList:

CAMLQuery: "<Query><Where><Eq><FieldRef Name='RelatedItem' /><Value Type='Number'>" + related + "</Value></Eq></Where><OrderBy><FieldRef  Name='DiscussionLastUpdated' Ascending='FALSE' /></OrderBy></Query>"

And for creating discussion threads:

updates: "<Batch OnError='Continue' >" +
     "<Method ID='1' Cmd='New'>" +
      "<Field Name='ContentType'>Discussion</Field>" +
      "<Field Name='FSObjType'>1</Field>" +
      "<Field Name='Title'>" + escapeColumnValue(title) + "</Field>" +
      "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
      "<Field Name='RelatedItem'>" + related + "</Field>" +
     "</Method>" +
    "</Batch>"


In my project, I linked my discussion list to a Video Library, and each Video has an area for creating comments and reply to threaded discussions.  All the conversation are saved in one single Discussions List, but filtered on each page as required.

Understand the nested format for Discussion List items

In a SharePoint Discussion List:

  • Discussion (content type) is a Folder.  FSObjType = 1
  • Message (content type) is a List Item.  FSObjType = 0

 

Reply in a Discussion List.


var list = "My Discussions";
var filepath = "Company/Site1/Web1/Lists/My Discussions/15_.000";
var body = $("textarea.body").text();

function replyDiscussion(list, filepath, body) {

// RootFolder needs to start with /

var promise = $().SPServices({
  operation: "UpdateListItems",
  batchCmd: "New",
  listName: list,
  updates: "<Batch OnError='Continue' RootFolder='" + "/" + filepath + "'>" +  // RootFolder needs to start with /
     "<Method ID='1' Cmd='New'>" +
      "<Field Name='ContentType'>Message</Field>" +
      "<Field Name='FSObjType'>0</Field>" +
      "<Field Name='Body'>" + escapeColumnValue(body) + "</Field>" +
     "</Method>" +
    "</Batch>"
});
promise.fail(function(xhr, status, error) {
  alert(xhr.responseText);
});
promise.done(function(){
  getDiscussionReplies(list, filepath);
});

} // end replyDiscussion

 

Image882

Figure: Threaded Discussions, with inline reply.


Key points:

  • List.asmx - UpdateListItems
  • Set FSObjType = 0 (for List Item), this is necessary or there were head-scratching bugs
  • Set RootFolder to the Filepath of the parent Discussion (folder) item.  This way, UpdateListItems creates a new list item within that folder.

 

 

Conclusion: here we are.  Threaded, inline, replies.


Combine everything together, the javascript lets me now do:

  • Tie a Discussion List to a Picture Library
  • Start one or more discussion threads on any Picture
  • See replies inline in one single UI.
  • Inline comment-creation
  • Inline comment-reply

 

  • Inline Edit and Inline Delete is possible, but the end-user will need to follow SharePoint permission settings on the Discussion List.  I have not written code to do those operations. 

SPSSYD 2013 and special thanks to Brian Farnhill

I wanted to thank Brian Farnhill for organizing SharePoint Saturday (SPS) events in Australia faithfully for the last few years.  SharePoint Saturday Sydney 2013 was the last one where he is the official organizer, chief, keynoter, label-printer, sponsor-chaser, etc. etc.

The event had a lot of highlights for me:

  • A lot of people showed up early and was ready at the keynote. 
  • Coffee Cart showed up on time in the morning
  • The Clifton venue (they moved since last year) was amazing - I really liked the layout of the sofa and the meal table-benches were great for conversation
  • Lunch was hopefully just enough - I think right at the end we might have just ran out of sandwiches
  • Adam got rick rolled'
  • Ross' session had a memory moment (I heard second hand)
  • My session had a major projector fail (more on this later)

 

Presentation - Typescript PowerPoint and demo project

 

Explanation - What happened to the projector / your laptop?!

 

I was using a USB-3 display link adapter for the last month.  But I didn't realize it would behave very badly with the HDMI-VGA dongle for the projector at the event.  A quick fumbling got the display to work.  But it was black and white but I was going to run out of time so I just ran with it.

I'm glad you guys had laughs at my expense.  It made me feel a little bit less miserable.

Love you guys.

Using VSNET 2013 and write TypeScript directly to any SharePoint via WebDAV

Disclaimer:  This is not the recommended workflow.  As a developer you should use VS.NET 2013 and build SharePoint projects.  You can then use TypeScript in your solution.

This is just a REALLY nice way to be super productive while working on Javascript in SharePoint.  Additionally, it works with Office 365, SharePoint 2007, 2010, 2013.  Does not require you to have a SharePoint development environment with Visual Studio .NET installed.

 

What do you need:

  • A windows machine, with Visual Studio .NET 2013 (TypeScript is better with VSNET 2013)
  • You don't need SharePoint installed in this machine.  You also don't need to have the VS.NET SharePoint extensions installed.
  • You need TypeScript though, grab it from: http://www.typescriptlang.org/#Download

 

Change your VS.NET settings:

We will be opening TypeScript and Javascript files directly from SharePoint via WebDAV.  These files are not part of a VSNET project.  So we need to flick on an option within VS.NET to automatically compile TypeScript files which are not part of a project

image

 

Open... TypeScript!

So now you can open TypeScript directly from File > Open

image
Figure: VS.NET Open File Dialog

 

And as you work in VS.NET and save the TypeScript file, the Javascript is generated and populated next to the TypeScript file on SharePoint.

image
Figure: SharePoint TypeScript and Javascript files kept in sync by VS.NET

Similarly, you can create new TypeScript files in VS.NET and save it to SharePoint.

Your master page references the Javascript file.  So you can go back to the pages that is using the new function and just F5 to refresh it and see the new results.

 

Why is this cool?

 

  • You can use VS.NET 2013 on your client machine, and use the latest version of TypeScript regardless of what SharePoint server you are talking to.  You aren't restricted by SharePoint 2010 and needing VS.NET 2010, or using VS.NET 2008 with SharePoint 2007.  You don't need SharePoint installed locally and you don't need to have the SharePoint extensions installed for VS.NET 2013.  As a consultant this is a life saver - I can now use TypeScript on old 2010/2007 environments.
  • You get the productivity of being able to save (in VS.NET), refresh (in browser) and immediately see changes in your Javascript.  You don't have to package / deploy / update-spsolution etc
  • You get all the benefits of TypeScript, including reference paths (as long as you put the definition files in SharePoint too, in a relative location).
  • When you are ready to package and deploy, take the TypeScript file out and put it into a proper SharePoint solution.

Introduce your drastic UI changes... slowly

 

We are in the middle of a somewhat sudden rebranding exercise.  One of the main colours that was used prominently in the previous theme of the website is now being retired as the UI is simplified.

 

So here is what we have right now:

image

 

And here is where we're going (currently in development).

image

 

Where's the Yellow?

 

In every case where we ask one of our existing users the first response is always: Where's the "Company Yellow"?

There's of course a marketing message that will go out with our latest release. 

But here's the secret developer compromise.

 

We can reduce the yellow, slowly, overtime

 

The old header background will remain.  But in the Javascript that's run on every page, we introduce a small piece of script that modifies the opacity of the background header.

var days = (new Date("2013-08-20") - new Date()) / (24*60*60*1000);  // remaining days until 2013-08-20
var opacity = (days > 0) ?  (0.5  * (days / 14)) : 0;   // starts from 0.5 opacity to 0
$(".header-banner").css("opacity", opacity);

What this does, is that over the course of two weeks, the yellow's opacity reduces from the original (50% at this next release) to 0%.  So as days go on, the yellow header begins to fade, and after two weeks, it will simply disappear!

I wonder what'd be the water cooler conversation.  Each person will wonder what happened to the yellow as it slowly faded out of our lives.

Australian SharePoint Conference 2013 Melbourne

Thank you attendees for yet another super conference.  It was quite a lot of fun presenting TypeScript for SharePoint.

 

I've got lots of feedback regarding the section on JavaScript - and have heard you guys loud and clear:

  • While the examples are fun...
  • But if you have other stuff to talk about, skip it!

I will miss talking about hoisting.  One of the craziest JavaScript gotchas there ever was.

 

VS.NET Extension

 

Downloads