Apps, Apps And Apps … A Story (Step 2) – Commenting App

  1. Apps, Apps And Apps … A Story (Step 1)
  2. Apps, Apps And Apps … A Story (Step 2) – Commenting App – this blog
  3. Apps, Apps And Apps … A Story (Step 2b) – Commenting App Going AngularJS

So 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 Glimlach.

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

These are some of the updates that come to mind at the moment (writing this blog post while traveling back to Antwerp).

So let”s begin:

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 / generic) list to put our comments in.

So we add to the solution also this list ‘Comments’ of type Generic list.

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 content type 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 is added 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.

In the previous blog I have noted that a few things that the App for Office solution creates upon adding the solution to your SharePoint Apps Visual Studio Solution.

It also creates an OfficeAppManifest xml file, in this file you can see 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> and </body> tag we are going to put the below code snippet, you can remove what”s originally there.

 <!-- 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>

There is nothing really special about the code, the only things worth mentioning are:

  • the click event of the button is being linked when the document is fully initialized
  • the div with id ‘comments-message’ will be used to fill with comments.

In the App.css file I am going to add a separate CSS style for the comment boxes (to make the entire solution a little bit nicer).

#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;
}

again nothing fancy, I actually just copied the notification CSS styles and renamed a few things Knipogende emoticon .

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.

First things first, we need an object, in case you are wondering why the strange notations and stuff, I am using the Revealing Method 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
    createItem = createItem = function (title, reviewer, comments, filename) {

    },
    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;
            }
        });
    },
    setAppUrl = function (appweburl) {
        // one time set of the app url
        appURL = appweburl;
    },
    getAllByDocument = function (documentName) {

    },
    updateItem = function (id, reviewer, comments) {

    },
    removeItem = function (id) {

    },
    getByCommentID = function (docID) {

    }

    //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 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

    // 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 using 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();
                                });
                            });
                    });
            }

Let me explain a few things what I am doing here first.

For some basic information you can continue to read here, for a more general overview I would like to refer you to this msdn page.

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. ‘Document’ is a global variable and it contains the word document.

App.Initialize is something that is already there one level up, you can see in the html a reference to app.js . Some div”s are being appended to the body node. (not really needed because I don”t use it now)

The part after that with ‘close_box”‘ is so that I can close my comments and in doing so I am retrieving an item ID and I am passing it to my object so that the comment is being deleted from the list.

In the if 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 if you would save the file the Office App isn”t being ‘relaunched’ so it actually still doesn”t know the filename. When you close your word client and open the document again, than it will find a url that contains a filename.

Keep in mind that I am referring to filename and not the title field. When you save a word document (any office document for that mather), the title field is never filled in, only the name field.

The reason why I am mentioning this is that I wanted to make a clean solution, using lookups and such, but the REST and lookups to the name fields , not easy (didn”t get it working).

Now that I am using CSOM stuff should work now but I”ll revise it in a later stadium.

Ok back to the coding part…

If the url is empty than I am not showing anything except a message that the comment app cannot be used with new documents.

The url is not empty than add a click event to the button and deferred load SP javascripts, get the users from SharePoint and check if the list already contains comments for the document.

 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());
                }
            }

First is getting the url from the app web and putting this as a global variable in the current function and in the object.

   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);

            }

The addItem function calls the createComment method in the object and passes the information from the app like who is the reviewer, what”s the comment message and what is the filename of the document.

After the create item, clear the text in the text field.

Getcomments get”s all the comments by passing the filename. DOM manipulation is being done in the method getAllByDocumentName . Not best practice I know Glimlach .

 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>');
                }
            }

Now we are going to get the site Users from SharePoint and this is done via a REST call. The results are than being appended to the dropdownlist.   

Keep in mind, if you are using REST and expect a Json format returned, you must add odata=verbose in the Accept header.

Ok, now let”s go back to our object, we still need to put all the logic in so that we have our CRUD operations working.

 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
        }

As you can see above, I am using CMOS to add an item to the list. Each time I go and get the content via the SharePoint Javascript API (remember the deferred load).

Special stuff here is ‘new SP.ListItemCreationInformation()’ ,  if you want more information just click the link Knipogende emoticon.

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, the effect this has is when hitting the add button, It is also updated in the comments list, so show your new comment immediately.

  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);
        }

The procedure to get all the comments for the document is shown above, nothing really fancy here, just launching a REST query and get comments.

The line that is commented out shows the way how to do the REST call to a lookup field. You must add the $expand stuff in between and also do a fieldname/lookupfieldname otherwise it will not work.

This didn”t work for me with the name field, didn”t really had the time to test as to why though. If someone knows the answer please put in a comment. Another option of course is using CSOM here Glimlach. But I just wanted to vary a little bit in using technologies.

In the success method I am doing some DOM manipulation, know that this probably isn”t the best place to do this. I am doing DOM manipulations from inside my object (which isn”t really allowed if you follow certain development patterns).

 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();
    },

And as final we have the removal of the comment.

An id is provided and via CSOM this is super easy, just get the item by ID and use ‘deleteObject()’. Call the executeQueryAsync to commit the change and your comment is deleted.

Hope this is helpful to some one Glimlach , if you have any questions just drop me a line. Next blog post will contain the same solution but done via AngularJS.

Download the code

Leave a Reply

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