This is part of a series:
- Apps, Apps And Apps … A Story (Step 1)
- Apps, Apps And Apps … A Story (Step 2) – Commenting App - this post
- Apps, Apps And Apps … A Story (Step 2b) – Commenting App Going AngularJS
In the previous blog post we’ve created a SharePoint App with an Office App embedded; this is called a composite model. Now we are going to create a real life example of this model that has a way of commenting an entire document.
There are additional updates that you can provide but this would overshoot the example, so I leave it up to the community to add functionality. Some of the updates that can be done:
- Update comments
- Create Tasks in SharePoint for the reviewers
- Use lookups instead of text fields
- Comment only a selection
Please note that I am not a JavaScript specialist and I created this in a few days time for demo purpose. Some of the coding you’ll see will be not best practices for coding JS but it will give you a good idea what it does.
We almost have all the key ingredients for our commenting app. But we still need one more custom list to put our comments in. So we add to the solution a Generic List named Comments.
In the schema.xml we are going to add a content type with the fields. You can create a separate content type and bind this to the comments list. The code snippet below will be added just above <ContentTypeRef ID='0x01'>.
<ContentType ID='0x01003A0615B3B7374E358B9C70938B67229B' Name='CommentsCT'>
<FieldRefs>
<FieldRef ID='{fa564e0f-0c70-4ab9-b863-0177e6ddd247}' Name='Title' />
<FieldRef ID='{B63E2472-AEAB-497C-B2D8-89540E6C2080}' Name='DocumentComment' />
<FieldRef ID='{84FAA816-776B-45A6-913F-31FF54366C1E}' Name='DocumentLookupColumn' />
<FieldRef ID='{F2849C6A-61EE-46A3-BD48-50EE3CAB2FC1}' Name='DocumentFileName' />
<FieldRef ID='{8F910850-62E3-47E9-AF31-4F44DC96F783}' Name='Reviewer' />
</FieldRefs>
</ContentType>
Next, add the fields in the <Fields> tag below the title field.
<Fields>
<Field ID='{fa564e0f-0c70-4ab9-b863-0177e6ddd247}' Type='Text' Name='Title' DisplayName='$Resources:core,Title;' Required='TRUE' SourceID='http://schemas.microsoft.com/sharepoint/v3' StaticName='Title' MaxLength='255' />
<Field ID='{B63E2472-AEAB-497C-B2D8-89540E6C2080}' Type='Text' Name='DocumentComment' DisplayName='DocumentComment' Required='False' />
<Field ID='{8F910850-62E3-47E9-AF31-4F44DC96F783}' Type='Text' Name='Reviewer' DisplayName='Reviewer' Required='False' />
<Field ID='{F2849C6A-61EE-46A3-BD48-50EE3CAB2FC1}' Type='Text' Name='DocumentFileName' DisplayName='DocumentFileName' Required='False' />
<Field ID='{84FAA816-776B-45A6-913F-31FF54366C1E}' Type='Lookup' Name='DocumentLookupColumn' DisplayName='Document Lookup Column' Required='FALSE' ShowField='Title' List='Lists/ConnectionsDocLib' />
</Fields>
I have added the lookup field already in case you want to use it in an update. It is also an option to add the fields to the <ViewFields> tag as well. Upon deployment you already have your fields visible in a view.
Next is to add another link to the comments list in the default.aspx.
The OfficeAppManifest.xml file contains the location of the html file: <SourceLocation DefaultValue='~appWebUrl/AppsDemo/Home/Home.html' />. So it already uses the ~appWebUrl tag to find the App web url.
The Home.html contains a script link to the Office CDN for the office.js file. You just have to comment this line and uncomment the 2 lines below.
Between the <body> tags we are going to put the below code snippet:
<!-- Page content -->
<div id='content-header'>
<div class='padding'>
<h1>Welcome</h1>
</div>
</div>
<div id='content-main'>
<div class='padding'>
Select Reviewer:
<select class='select' id='select-reviewer' name='D1'></select>
</div>
<div id='AddComments' class='padding'>
<textarea style='width:auto;' cols='40' rows='10' id='addCommentText'></textarea>
<br />
<button id='add-comment-to-list'>Add Item to List</button>
</div>
<div id='comments-message'></div>
</div>
In the App.css file I am going to add a separate CSS style for the comment boxes:
#comments-message {
background-color: #818285;
color: #fff;
position: absolute;
width: 100%;
min-height: 80px;
max-height:300px;
overflow-y:scroll;
right: 0;
z-index: 100;
bottom: 0;
display: none; /* Hidden until invoked */
}
#comments-message-header {
font-size: medium;
color: #fff;
margin-bottom: 10px;
}
#comments-message-body {
color: #fff;
}
Now for the real magic, we are going to completely clean the home.js file and going to fill it up with some JavaScript and jQuery using the Revealing Module Pattern.
This is a basic object with the method names but the methods themselves are not filled in yet.
var CommentsApp = window.CommentsApp || {};
CommentsApp.CommentsList = function () {
var digest;
var sp_context;
var appURL;
//private members
var createItem = function (title, reviewer, comments, filename) {
// ... implementation below
};
var getDigest = function () {
// get context first
$.ajax({
url: appURL + '/_api/contextinfo',
type: 'POST',
contentType: 'application/x-www-url-encoded',
headers: {
'accept': 'application/json;odata=verbose'
},
success: function (data) {
if (data.d) {
digest = data.d.GetContextWebInformation.FormDigestValue;
}
},
error: function (xhr) {
return xhr.status + ': ' + xhr.statusText;
}
});
};
var setAppUrl = function (appweburl) {
// one time set of the app url
appURL = appweburl;
};
var getAllByDocument = function (documentName) {
// ... implementation below
};
var updateItem = function (id, reviewer, comments) {
// ... implementation below
};
var removeItem = function (id) {
// ... implementation below
};
var getByCommentID = function (docID) {
// ... implementation below
};
//public interface
return {
createComment: createItem,
updateComment: updateItem,
deleteComment: removeItem,
getByCommentID: getByCommentID,
setAppWebUrl: setAppUrl,
getAllByDocumentName: getAllByDocument
}
}();
Below we are going to set an anonymous function that contains the initialize function and more stuff that we need when the page loads.
(function () {
'use strict';
var appWebURL;
var web;
var fileName;
// The initialize function must be run each time a new page is loaded
Office.initialize = function (reason) {
$(document).ready(function () {
app.initialize();
$(document).on('click', '.close_box', function () {
$(this).parent().fadeTo(300, 0, function () {
var id = $(this).find('#commentID').text();
$(this).remove();
CommentsApp.CommentsList.deleteComment(id);
})
});
if (Office.context.document.url == '') {
$('#AddComments').empty();
$('#content-main > .padding').empty();
$('#comments-message').append('<div class="padding">' +
'<div id="comments-message-header">New document</div>' +
'<div id="comments-message-body"> To use commenting, load an existing document. </div>' +
'</div>');
$('#comments-message').slideDown('fast');
}
else {
fileName = Office.context.document.url.toString().substr(Office.context.document.url.toString().lastIndexOf('/') + 1);
$('#add-comment-to-list').click(addItem);
var scriptbase = '/_layouts/15/';
$.getScript(scriptbase + 'SP.Runtime.js',
function () {
$.getScript(scriptbase + 'SP.js',
function () {
getAppWeb(function () {
getSPUsers(populateUsersDropDown);
getComments();
});
});
});
}
});
};
function getAppWeb(functionToExecuteOnReady) {
var context = SP.ClientContext.get_current();
web = context.get_web();
context.load(web);
context.executeQueryAsync(onSuccess, onFailure);
function onSuccess() {
appWebURL = web.get_url();
CommentsApp.CommentsList.setAppWebUrl(appWebURL);
functionToExecuteOnReady();
}
function onFailure(sender, args) {
app.initialize();
app.showNotification('Failed to connect to SharePoint. Error: ' +
args.get_message());
}
}
function addItem(comments) {
// Calls the create
CommentsApp.CommentsList.createComment('Comment', $('#select-reviewer option:selected').text(), $('#addCommentText').val(), fileName);
$('addCommentText').empty();
}
function getComments() {
var results = CommentsApp.CommentsList.getAllByDocumentName(fileName);
}
function getSPUsers(functionToExecuteOnReady) {
var url = appWebURL + '/../_api/web/siteUsers';
jQuery.ajax({
url: url,
type: 'GET',
headers: {
'ACCEPT': 'application/json;odata=verbose'
},
success: onSuccess,
error: onFailure
});
function onSuccess(data) {
var results = data.d.results;
functionToExecuteOnReady(results);
}
function onFailure(jaXHR, textStatus, errorThrown) {
var error = textStatus + ' ' + errorThrown;
app.showNotification(error);
}
}
function populateUsersDropDown(results) {
for (var i = 0; i < results.length; i++) {
var IDTemp = results[i].Id;
$('#select-reviewer').append('<option value="' + IDTemp + '">' +
results[i].Title + '</option>');
}
}
})();
I am using the Office namespace that contains an initialize method. This is being fired when the Office API is loaded. After that I am using jQuery to check when the page is fully loaded for DOM manipulation.
In the if statement I am checking of the url is empty or not. The reason why is, when you open a new document directly from the document library, it doesn’t contain a filename. So you can’t add a comment because of the lack of file name.
Now we fill in the CRUD operations in our object.
// Inside CommentsApp.CommentsList = function () { ...
createItem = function (title, reviewer, comments, filename) {
// function that creates a comment item in the comment list
// get client context
sp_context = new SP.ClientContext(appURL);
// Get the list comments
var list = sp_context.get_web().get_lists().getByTitle('Comments');
// add a new item to the list
var comment = list.addItem(new SP.ListItemCreationInformation());
// fill the item values
comment.set_item('Title', title);
comment.set_item('DocumentComment', comments);
comment.set_item('Reviewer', reviewer);
comment.set_item('DocumentFileName', filename);
comment.update();
// commit the changes
sp_context.executeQueryAsync(onQuerySucceeded, onQueryFailed);
function onQuerySucceeded(sender, args) {
// need to wait on success before filling the comments
getAllByDocument(filename);
}
function onQueryFailed(sender, args) {
// capture in case of fail
}
};
getAllByDocument = function (documentName) {
var url = appURL + "/_api/web/lists/getbytitle('Comments')/Items?$select=Title,ID,DocumentComment,DocumentFileName&$filter=DocumentFileName eq '" + documentName + "'";
// below is an example on how to use REST with a lookup field, I had an issue with /Name though
// var url = appURL + "/_api/web/lists/getbytitle('Comments')/Items?$select=Title,DocumentComment,DocumentLookupColumn/Title&$expand=DocumentLookupColumn/Title&$filter=DocumentLookupColumn/Title eq '" + documentName + "'";
$.ajax({
url: url,
type: 'GET',
headers: { 'accept': 'application/json;odata=verbose' },
success: function (data) {
if (data.d.results) {
onSuccess(data.d.results);
}
},
error: onError
});
function onSuccess(results) {
$('#comments-message').empty();
for (var i = 0; i < results.length; i++) {
$('#comments-message').append('<div id="box"><div class="close_box"><img src="' + appURL + 'ConnectionsOfficeApp/Home/images/close-icon.png"/></div><div class="padding">' +
'<div id="comments-message-header">' + 'Comments #' + i.toString() + ': ' + '</div>' +
'<div id="comments-message-body"> ' + results[i].DocumentComment + ' </div>' +
'<div id="commentID" style="visibility:hidden;">' + results[i].ID + '</div>' +
'</div></div>');
}
$('#comments-message').slideDown('fast');
}
function onError(jaXHR, textStatus, errorThrown) {
var error = textStatus + ' ' + errorThrown;
app.showNotification(error);
}
};
removeItem = function (id) {
sp_context = new SP.ClientContext(appURL);
var comment = sp_context
.get_web()
.get_lists()
.getByTitle('Comments')
.getItemById(id);
// delete the selected item
comment.deleteObject();
sp_context.executeQueryAsync();
};
// ... rest of the object
With set_item we can set the field values of that item. The changes are only pushed back to SharePoint when doing executeQueryAsync. Because It is async we need a succeed and failure method to capture the return. In the success method I am getting all the comments for that document again, so your new comment shows immediately.
Hope this is helpful to some one. If you have any questions just drop me a line. Next blog post will contain the same solution but done via AngularJS.