Attaching a file to a list item in SharePoint add-in model

Long time since I wrote about the SharePoint. Is still my job, I am still working on this, but I simply did not find the time to discover new things. Now, as SharePoint add-in model is more and more adopted in SharePoint development, due to the migration of large companies to Office 365, I am starting to face new challenges. One of this is how to attach a file to an existing list item.

<input type="file" id="attachment1" />

As HTML5 is the new standard for web, file control is able to read the content of a file as a binary string, so no more needed to server side languages. For rest, jQuery and SharePoint JavaScript libraries will do the work for you.


       Type.registerNamespace('Shp');

       Shp.Attachments = function () {
            /// <summary>Shp Attachments static class</summary>
            throw 'Cannot initiate Shp.Attachments static class';
        };

        Shp.Attachments.get_file = function (fileInput) {
            /// <summary>This method is used to get the content of the file as binary string</summmary>

            var deffered = jQuery.Deferred();
            var reader = new FileReader();
            reader.onload = function (e) { 
                deffered.resolve(e.target.result);
            };  

            reader.onerror = function (e) {
                deffered.reject(e.target.error);
            };

            reader.readAsBinaryString(fileInput.files[0]);
            return deffered.promise();
        };


        Shp.Attachments.add = function (listName, itemId, fileInput, webUrl, success, fail) {
            /// <summary>Add attachments</summary>
            /// <param>List name</param>
            /// <param>Item Id</param>
            /// <param>File input controls</param>
            /// <param>Web url</param>
            var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                            { name: 'itemId', type: String, optional: false, mayBeNull: false },
                                                            { name: 'filesInput', type: HTMLElement, optional: false, mayBeNull: false },
                                                            { name: 'webUrl', type: String, optional: false, mayBeNull: true },
                                                            { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                            { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
            if (e) throw e;


            var webUrl = webUrl || _spPageContextInfo.webAbsoluteUrl;
            var fail = fail || function (err) { alert(err); };

            Shp.Attachments.get_file(fileInput).then(function (fileContent) {
                var parts = fileInput.value.split("\\");
                var fileName = parts[parts.length - 1];
                // Attachments add internal method
                Shp.Attachments._add(listName, itemId, fileContent, fileName, webUrl, success, fail);
            });
        };

        Shp.Attachments._add = function (listName, itemId, fileContent, fileName, webUrl, success, fail) {

            var scriptBase = webUrl + "/_layouts/15/";
            jQuery.getScript(scriptBase + "SP.RequestExecutor.js", function () {

                var executor = new SP.RequestExecutor(webUrl);
                executor.executeAsync({
                    url: webUrl + "/_api/web/lists/GetByTitle('" + listName + "')/items(" + itemId + ")/AttachmentFiles/add(FileName='" + fileName + "')",
                    method: "POST",
                    binaryStringRequestBody: true,
                    body: fileContent,
                    state: "Update",
                    success: function () {
                        success(itemId);
                    },
                    fail: function (data) {
                        fail(data.responseText);
                    }

                });

            });
        };

        Shp.Attachments.registerClass('Shp.Attachments');

Once code is organized, attaching a file become an easy task.

 Shp.Attachments.add('Tickets', '2', document.getElementById('attachment1'), null, function (itemId) {
        alert(itemId);
    }, function (err) {
        alert('Error: ' + err);
    });

Shp.Attachments.add accepts following parameters:

  • List name as string
  • Item id as string
  • HTML file control as HTML element
  • Web URL. It can be nul and in this case is used _spPageContextInfo.webAbsoluteUrl
  • Success method to be executed if attachment is added
  • Fail method to be executed if attachment failed

Copy, adapt the code according to your needs and use it. Happy coding! If it helps, you can just say hi to me and I will be happy as well. 🙂

Advertisements

Adding items in multiple lists using JavaScript CSOM

In general my ideas for my posts are generated by problems I am facing in my development activity. One of the problems working JavaScript CSOM is sometimes projects require very complex data operations, like adding items into multiple SharePoint lists. I would not call this a challenge, as you always can use classical examples and perform insert operations one by one, waiting for previous operation to be completed before starting a new one. But why not make it easier?

If you want to achieve this in a simple way, just take a look on the code below.

Type.registerNamespace('Shp');


Shp.Lists.AddItemsInLists = function (addItemsArguments, webUrl, success, fail) {
    /// <summary>Add items in multiple lists calling execute query async once</summary>
    /// <param name="addItemsArguments" type="Array" elementType="Object" elementMayBeNull="false" mayBeNull="false" optional="false">Add items arguments</param>
    /// <param name="webUrl" type="String" mayBeNull="true" optional="false">Web url</param>
    /// <param name="success" type="Function" mayBeNull="false" optional="false">Success</param>
    /// <param name="fail" type="Function" mayBeNull="false" optional="true">Fail</param>
    var e = Function.validateParameters(arguments, [{ name: 'addItemsArguments', type: Array, elementType: Object, elementMayBeNull: false, optional: false, mayBeNull: false },
                                                    { name: 'webUrl', type: String, optional: false, mayBeNull: true },
                                                    { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                    { name: 'fail', type: Function, optional: true, mayBeNull: false }], true);
    if (e) throw e;
    var ctx = (typeof webUrl === 'undefined' || webUrl === null) ? SP.ClientContext.get_current() : new SP.ClientContext(webUrl);
    var fail = fail || function (err) { alert(err); };
    Shp.Lists._AddItemsInLists(addItemsArguments, ctx, success, fail);
};


Shp.Lists._AddItemsInLists = function (addItemsArguments, ctx, success, fail) {
    var results = {}, current, i, k;

    for (i = 0; i < addItemsArguments.length; i++) {

        current = addItemsArguments[i]; // Store current item

        // We check if list name property if defined before do other operations
        if (current.hasOwnProperty("listName") === true) {

            if (results.hasOwnProperty(current.listName) === false) results[current.listName] = []; // Results object doesn't have a slot for current list name, create it
            var oList = ctx.get_web().get_lists().getByTitle(current.listName);

            // We check if list items property is an array
            if (current.hasOwnProperty("listItems") === true && isArray(current.listItems) === true) {
                for (k = 0; k < current.listItems.length; k++) {
                    var listItem = current.listItems[k];
                    var oListItem = oList.addItem(new SP.ListItemCreationInformation());
                    for (var field in listItem) {
                        oListItem.set_item(field, listItem[field]);
                    }
                    oListItem.update();
                    ctx.load(oListItem);
                    results[current.listName].push(oListItem);
                }
            }
        }
    }

    ctx.executeQueryAsync(function () {
        success(results);
    }, function (sender, args) {
        fail(args.get_message());
    });

};

Shp.Lists.registerClass('Shp.Lists');

I have created a method “AddItemsInLists” for “Shp.Lists”, which is accepting 4 parameters:

  • addItemsArguments – which is an array of objects containing information about list items you want to insert.
  • webUrl – which is the URL of the web inside current site collection. If null, web for current context is considered.
  • success – function to be executed if everything goes fine.
  • fail – function to be executed if fails. It is optional and if not specified a simple alert containing the error is shown.

Let’s concentrate on “addItemsArguments” parameter. As I said, it is an array of objects, but let me explain how these objects should look like. Basically it should contain 2 properties: “listName” (name of the list where you want to insert items) and “listItems” (another array of objects, this time representing the items).

var firstListItems = { listName: 'Name of the list', listItems: [{ 'Field 1': 'Value', 'Field 2': 'Value' }, { 'Field 1': 'Value', 'Field 2': 'Value'}]  };
var secondListItems = { listName: 'Name of the 2nd list', listItems: [{ 'Field 1': 'Value', 'Field 2': 'Value' }, { 'Field 1': 'Value', 'Field 2': 'Value'}]  };
var addItemsArguments = [firstListItems, secondListItems];

Now calling the method I have created is easy and no rocket science is required.

Shp.Lists.AddItemsInLists([firstListItems, secondListItems, ], null, function (results) {
    /*  Do something with the results.
        You can get items inserted in a list like this: results['name of the list']  */
}, function (err) {
    alert(err); // Alert the error or do something elsel
});

Stop SharePoint converting dates to GMT timezone

I often used with SharePoint custom forms and in general I based my development on SharePoint JSOM. Working with it, I am often requested to update date and time fields using JSOM. But there is a small issue with it.

var dt = new Date();
item.set_item(dt);

The code above is with working, with an exception. It converts the date to GMT timezone. If my timezone is GMT + 2, recorded date will have 2 hours less then I have specified. The workaround is to convert the date into formatted string.

var dt = new Date();
item.set_item(dt.format('yyyy-MM-ddTHH:mm:ssZ'));

Date format function is provided by MicrosoftAjax, which is included by default.

Create document set with custom properties using JSOM

This is article is not about something new, is about how you organize the code to create a document set with custom properties. Let’s imagine I have created a column “Employee” (people or group field type) and associate it with Document Set content type. Of course I want to assign a value to this field when I create a new document set. What I did was to get the pieces of information from the internet and consolidate my code based on these.

Shp.DocumentSet = function () {
    throw 'Cannot instantiate Shp.DocumentSet static class';
}

Shp.DocumentSet.createDocumentSet = function (listName, docSetName, web, properties, success, fail) {
    /// <signature>
    ///	    <summary>Create document set</summary>
    /// 	<param name="listName" type="String" optional="false" mayBeNull="false">Document library name</param>
    /// 	<param name="docSetName" type="String" optional="false" mayBeNull="false">Document set name</param>
    ///     <param name="web" type="SP.Web" optional="false" mayBeNull="true">Web</param>
    ///     <param name="success" type="Function" optional="false" mayBeNull="false">Success</param>
    ///     <param name="error" type="Function" optional="true" mayBeNull="false">Fail</param>
    /// </signature>

    var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                     { name: 'docSetName', type: String, optional: false, mayBeNull: false },
                                                     { name: 'web', type: SP.Web, optional: false, mayBeNull: true },
                                                     { name: 'properties', type: Object, optional: false, mayBeNull: true },
                                                     { name: 'success', type: Function, optional: false, mayBeNull: false },
                                                     { name: 'fail', type: Function, optional: false, mayBeNull: true }], true);
    if (e) throw e;


    var ctx = (typeof web === 'undefined' || web === null) ? SP.ClientContext.get_current() : web.get_context();
    var web = (typeof web === 'undefined' || web === null) ? ctx.get_web() : web;
    var fail = fail || function (err) { alert(err); }

    Shp.DocumentSet._createDocumentSet(listName, docSetName, ctx, web, properties, success, fail);

};

Shp.DocumentSet._createDocumentSet = function (listName, docSetName, ctx, web, properties, success, fail) {

    var list = web.get_lists().getByTitle(listName);
    ctx.load(list);

    var parentFolder = list.get_rootFolder();
    ctx.load(parentFolder);



    var docSetContentTypeID = "0x0120D520";
    var docSetContentType = ctx.get_site().get_rootWeb().get_contentTypes().getById(docSetContentTypeID);
    ctx.load(docSetContentType);

    ctx.executeQueryAsync(function () {
        SP.DocumentSet.DocumentSet.create(ctx, parentFolder, docSetName, docSetContentType.get_id());
        var docSetFolder = web.getFolderByServerRelativeUrl(parentFolder.get_serverRelativeUrl() + '/' + docSetName);
        var docSetFolderItem = docSetFolder.get_listItemAllFields();
        if (properties != null) {
            for (var property in properties) {
                if (properties.hasOwnProperty(property) === true) {
                    docSetFolderItem.set_item(property, properties[property]);
                }
            }
        }
        docSetFolderItem.update();
        ctx.load(docSetFolderItem);
        ctx.executeQueryAsync(function () {
            success(docSetFolderItem);
        }, fail);
    },
    fail);

};

Shp.DocumentSet.registerClass('Shp.DocumentSet');

I have created a static class called Shp.DocumentSet with Shp.DocumentSet.createDocumentSet method, which accepts these parameters:

  • listName: document library name
  • docSetName: document set name
  • docSetName: SP.Web I want to use for current operation. If null, web for current context will be considered.
  • properties: an object containing custom properties I want to update. If null, no custom property will be updated.
  • success: function to be called on success. This will be called passing list item associated with document set folder as a parameter.
  • fail: function to be called on fail.

Let’s see how you can use it.

var docLibraryName = 'Doc library name';
var docSetName = 'Doc set name';
var spWeb = null;
var properties = {};
properties['Employee'] = employeeId;
var success = function(item) {
    // Do something with item
};

var fail = function (err) {
    // Do something with error
};

Shp.DocumentSet.createDocumentSet(docLibraryName, docSetName, spWeb, properties, success, fail);

Creating document set with custom property is now a simple a operation. I do not need write a lot of code each time I need to this.

Autocomplete textbox with JavaScript CSOM

I have searched on the internet about how to create an auto-complete functionality in SharePoint. Of course, jQuery UI was the solution with an Ajax request to REST service. For some reason, I cannot understand it, the examples I have seen are based on synchronous Ajax request. So I simply said no way. I needed something asynchronous to avoid page freeze.

Normally to create an auto-complete is a simple thing.


    jQuery('#txtBox').autocomplete({
        minLength: 3,
        source: function(request, response) {
         // At the and of the async operation call response with obtained results
         }
     });

Asynchronous operation will be placed inside source function, but instead using classic Ajax examples, I will use JavaScript CSOM. It is not better, but I like it more. So is more a personal choice.

For getting data from SharePoint, you can use classical example from MSDN website, but I prefer to reorganize the code a little bit. After all I still have Microsoft Ajax library available so I can put classes in namespaces or I can validate parameters type.

/// <Reference Name="MicrosoftAjax.js" />


Type.registerNamespace('Shp');


Shp.Lists = function () {
    throw 'Cannot instantiate Shp.Lists static class';
}


Shp.Lists.GetItems = function (listName, query, web, success, fail) {
    /// <summary>Get list items based on provided CAML query</summary>
    /// <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
    /// <param name="query" type="String" optional="false" mayBeNull="false">Query</param>
    /// <param name="web" type="SP.Web" optional="false" mayBeNull="true">Web</param>
    /// <param name="success" type="Function" optional="false" mayBeNull="false">Success callback</param>
    /// <param name="fail" type="Function" optional="true" mayBeNull="false">Fail callback</param>

    var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, mayBeNull: false, optional: false },
                                                   { name: 'query', type: String, mayBeNull: false, optional: false },
                                                   { name: 'web', type: SP.Web, mayBeNull: true, optional: false },
                                                   { name: 'success', type: Function, mayBeNull: false, optional: false },
                                                   { name: 'fail', type: Function, mayBeNull: false, optional: true }], true);
    if (e) throw e;

    var fail = fail || function (error) { alert(error); };
    var ctx = (web === null) ? SP.ClientContext.get_current() : web.get_context();
    var web = (web === null) ? ctx.get_web() : web;


    Shp.Lists._GetItems(listName, query, ctx, web, success, fail);
}

Shp.Lists._GetItems = function (listName, query, ctx, web, success, fail) {

    var oList = web.get_lists().getByTitle(listName);
    var camlQuery = new SP.CamlQuery();
    camlQuery.set_viewXml(query);
    var oListItems = oList.getItems(camlQuery);
    ctx.load(oListItems);
    ctx.executeQueryAsync(function () {
        success(oListItems);
    }, function (sender, args) {
        fail(args.get_message());
    });
}

Shp.Lists.registerClass('Shp.Lists');

“Shp.Lists.GetItems” should be called with the following parameters:

  • List name, as string, not optional and cannot be null.
  • CAML query as string, not optional and cannot be null.
  • Web as SP.Web, not optional but can be null. In this case web associated with the current context is used.
  • Success as function, not optional and cannot be null. It is executed if operation is a success.
  • Fail function, optional and cannot be null. If not specified and operation fails, code will alert the error message.

Now as I created a reusable function for reading list items, everything should be much easier. I just need to call “Shp.Lists.GetItems” with correct parameters inside auto-complete source and, if operation is successfully, to add suggestions based on list items.

jQuery('#txtBox').autocomplete({
    minLength: 3,
    source: function (request, response) {
       
        var term = request.term;
        var query = '<View><Query><Where><Contains><FieldRef Name="Title" /><Value Type="Text">' + term + '</Value></Contains></Where></Query></View>';
        Shp.Lists.GetItems("list name", query, null, function (items) {
            var suggestions = [];
            var listItemEnumerator = items.getEnumerator();
            while (listItemEnumerator.moveNext()) {
                suggestions.push(listItemEnumerator.get_current().get_item('Title'));
            }
            // Add suggestions
            response(suggestions);

        });

    }
});

This was my approach of creating the auto-complete functionality. As I said, is a personal option to use JavaScript CSOM because looks for me more organized and structured. Of course code can be extended and you can even create an Ajax client side control to incorporate this functionality.

Thank you for reading my post!

Send list items to recycle bin with JavaScript CSOM

Delete operations examples provided by Microsoft include how to delete list items using JavaScript CSOM. And everything is fine, with a small exception. “deleteObject” operation is completely deleting items and you cannot restore them from recycle bin. But instead you can use “recycle” method of SP.ListItem object. My article will show how to sent multiple items to recycle bin using JavaScript CSOM.

As usual, I am using Microsoft Ajax to organize my code in namespaces and classes:

/// <Reference Name="MicrosoftAjax.js" />
/// <Reference Path="jquery.js" />
Type.registerNamespace('Shp');

Shp.Lists = function () {
    throw 'Cannot instantiate Shp.Lists static class';
}

Shp.Lists.registerClass('Shp.Lists');

I will have a static class called “Lists” inside “Shp” namespace and with some help from jQuery (I will need it for deferred object) I will add my methods to complete the operations. In my case method will be called “RecycleItems”.

/// <Reference Name="MicrosoftAjax.js" />
/// <Reference Path="jquery.js" />

Type.registerNamespace('Shp');


Shp.Lists = function () {
    throw 'Cannot instantiate Shp.Lists static class';
}

Shp.Lists.RecycleItems = function (listName, listItems, web) {
    ///	 <summary>Recycle items from the list</summary>
    ///  <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
    ///  <param name="listItems" type="Array" elementType="Number" elementInteger="true" elementMayBeNull="false" optional="false" mayBeNull="false">Array with string representation of list items id</param>
    ///  <param name="web" type="SP.Web" optional="true" mayBeNull="false">Web</param>
    ///  <returns type="jQuery.deffered" />
    var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                   { name: 'listItems', type: Array, elementType: Number, elementInteger: true, elementMayBeNull: false, optional: false, mayBeNull: false },
                                                   { name: 'web', type: SP.Web, optional: true, mayBeNull: false }], true);
    if (e) throw e;

    // Depending if I provided web parameter, I build internal variable to call internal method
    var ctx = (typeof web === 'undefined' || web === null) ? SP.ClientContext.get_current() : web.get_context();
    var web = (typeof web === 'undefined' || web === null) ? ctx.get_web() : web;

    // Call and return internal method result, which is a jQuery.deffered
    return Shp.Lists._DefferedRecycleItems(listName, listItems, ctx, web);

}

Shp.Lists._DefferedRecycleItems = function (listName, listItems, ctx, web) {
    // Implementation will go here
}

Shp.Lists.registerClass('Shp.Lists');

My method is validating parameters types and called an internal method if parameters are in expected format. Last parameter, which is SP.Web, is optional. If you do not provide it, code will use website that is associated with the client context. And entire implementation will go into my internal method.

/// <Reference Name="MicrosoftAjax.js" />
/// <Reference Path="jquery.js" />

Type.registerNamespace('Shp');


Shp.Lists = function () {
    throw 'Cannot instantiate Shp.Lists static class';
}

Shp.Lists.RecycleItems = function (listName, listItems, web) {
    ///	 <summary>Recycle items from the list</summary>
    ///  <param name="listName" type="String" optional="false" mayBeNull="false">List name</param>
    ///  <param name="listItems" type="Array" elementType="Number" elementInteger="true" elementMayBeNull="false" optional="false" mayBeNull="false">Array with string representation of list items id</param>
    ///  <param name="web" type="SP.Web" optional="true" mayBeNull="false">Web</param>
    ///  <returns type="jQuery.deffered" />
    var e = Function.validateParameters(arguments, [{ name: 'listName', type: String, optional: false, mayBeNull: false },
                                                   { name: 'listItems', type: Array, elementType: Number, elementInteger: true, elementMayBeNull: false, optional: false, mayBeNull: false },
                                                   { name: 'web', type: SP.Web, optional: true, mayBeNull: false }], true);
    if (e) throw e;

    // Depending if I provided web parameter, I build internal variable to call internal method
    var ctx = (typeof web === 'undefined' || web === null) ? SP.ClientContext.get_current() : web.get_context();
    var web = (typeof web === 'undefined' || web === null) ? ctx.get_web() : web;

    // Call and return internal method result, which is a jQuery.deffered
    return Shp.Lists._DefferedRecycleItems(listName, listItems, ctx, web);

}

Shp.Lists._DefferedRecycleItems = function (listName, listItems, ctx, web) {

    var deferred = jQuery.Deferred();
    var oList = web.get_lists().getByTitle(listName);
    var recycledItems = [];

    // For each provided list item id we perform recycle method
    for (var i = 0; i < listItems.length; i++) {
        var oListItem = oList.getItemById(listItems[i]);
        var recycleItem = oListItem.recycle();
        recycledItems.push(recycleItem);
    }

    ctx.executeQueryAsync(function () {

        // In case of success, we refine the results into an array of GUID and use it with deferred.resolve
        Array.forEach(recycledItems, function (element, index, array) {
            array[index] = element['m_value']['_m_guidString$p$0'];
        }, null);

        deferred.resolve(recycledItems);

    }, function (sender, args) {

        // In case of fail, we use error message with deferred.reject
        deferred.reject(args.get_message());

    });

    return deferred.promise();

}

Shp.Lists.registerClass('Shp.Lists');

In my development tasks, code above provided an easy way to remove items from a list, being also capable to restore them. Without entering into details, I can show a basic example.

// Array containing ID of the list items to be sent to recycle bin
var deleteIds = ["1", "2", "3", "6"];

// Define delete operation
var deleteOps = Shp.Lists.RecycleItems('List Name', deleteIds);

// In case of fail we show error message
deleteOps.fail(function (error) {
    alert(error)
});

// In case of success, we show  identifier (GUID type) of the new recycle bin item
deleteOps.done(function (listItemsGuid) {
    for(var i = 0; i < listItemsGuid.length; i++)    {
        alert(listItemsGuid[i]);
    }
});

Hoping my code will help you, I will let you know continue your work. And thank you for ready my post. 🙂