In this post, I’m going to show how to do basic CRUD (Create, Read, Update, and Delete) operations with documents and SharePoint RESTful web services. Along the way, I’m going to flesh out possibly the world’s ugliest SPA (single page application). I’m only going to talk about the parts of the code that deal with Ajax and the RESTful web services, but I’ll attach the complete source. It’s a wiki page, so you can just drop it in a document library and open it to see how it works (it does try to work with a picture library with a title of Pictures in the current site, if you don’t have one, you can either create one or change the listTitle variable to be the title of another picture library in your site).
What We’re Going to Build
The HTML
There is nothing special about the HTML, but since I’m going to be manipulating it as I demonstrate the CRUD operations, I’ll show it here so you can refer back to it. It consists of an un-ordered list to show pictures currently in the library, a small form for edit/delete, and a div with some structure to implement drag and drop files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <!--List of pictures (name and radio button)--> <ul id="picturelist" style="list-style: none"> </ul> <!--Update Form--> <div id="updatePanel" class="updatepanel"> Title: <input type='text' id='title' val='' /> Description: <input type='text' id='description' val='' /> <button type='button' id='Update'> Update </button> <button type='button' id='Delete'> Delete </button> </p> <!--Drag and Drop div--> <div id="dragandrophandler"> <div id="draganddropbusy" style="display:none"> <!--Change this path for your environment--> <img src="/_layouts/images/progress.gif"> </p> <div id="draganddroplabel">Drag & Drop Images To Add</p> </p> |
As I process the read operation and shove list items into the un-ordered list, I’ll also add some hidden data- attributes to the item to store things like the title, description, id, and etag, that will be needed for other CRUD operations later on, so a list item will end up looking like this:
1 2 3 | <li data-id="333" data-title="Poipu - Hawaii" data-description="Snorkeling" data-etag="4"> <input name="item" id="item333" value="333" type="radio">Poipu_Hawaii.jpg </li> |
Ajax Wrapper
I wanted to do this tutorial without any dependencies, which isn’t that hard but doing Ajax with nothing but XMLHttpRequest is not fun. I don’t know any JavaScript developer who would argue otherwise. So the first thing I’m going to do is wrap XMLHttpRequest in a function called ajax, which takes in an options instance similar to the one used by jQuery’s $.ajax, and converts that instance to what XMLHttpRequest needs to perform an ajax request. The options can specify success and error callbacks, but I do not provide a way to use promises instead of callbacks, since that won’t work in IE without external dependencies. So here is my wrapper:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | function ajax(options) { // set some sensible defaults options.method = options.method || "GET"; options.headers = options.headers || {}; options.data = options.data || null; options.async = typeof(options.async) === "undefined" ? true : options.async; // foreach property of objects.headers, create a request header var keys = Object.keys(options.headers); var request = new XMLHttpRequest(); request.open(options.method, options.url, options.async); for (var i = 0; i < keys.length; i++) { request.setRequestHeader(keys[i].replace(/_/g, "-"), options.headers[keys[i]]); } // the function that will be the error callback var error = function() { // if options.error is a function if (typeof(options.error) === "function") { try { // if it looks like we should have a JSON response if ( options.headers.accept && options.headers.accept.indexOf("application/json") > -1 && request.responseText ) { // parse the error response as JSON if we can request.responseJSON = JSON.parse(request.responseText); } } catch (e) { // if not, create a response JSON indicating the problem, the original // response is still available as responseText request.responsJSON = { message: "Error parsing JSON in error: " + e.name + ": " + e.message }; } // call the options.error callback options.error(request); } }; var success = function() { // if the request status is outside the range of success if (request.status < 200 || request.status >= 400) { // make it callback error, this is a successful network request, and even a // successful http request, but it is a REST failure so treat it as such error(); } else if (typeof(options.success) === "function") { try { // if it looks like we should have a JSON response if ( options.headers.accept && options.headers.accept.indexOf("application/json") > -1 && request.responseText ) { // parse the response as JSON if we can request.responseJSON = JSON.parse(request.responseText); } // call the options.success callback options.success(request); } catch (e) { // if not, create a response JSON indicating the problem, the original // response is still available as responseText request.responseJSON = { message: "Error parsing JSON in success: " + e.name + ": " + e.message }; // pass it over to the error callback error(); } } }; // wire up the callbacks request.onload = success; request.onerror = error; // send the request request.send(options.data); } |
REST Create
Creating a document is fairly simple, but there are a number of headers that have to be just so. The code below does the following:
- A lot of the work is just constructing the Url. This REST call is getting the list by title, and then calling files/add on the root folder. It passes in two parameters, the file name and a Boolean indicating whether SharePoint should overwrite or fail if a file with the same name already exists. You also have to specify the request parameters @TargetLibrary and @TargetFileName. This is completely redundant; get used to it, there’s a lot of that in the REST API. Note: if you want to load documents to other folders, you can use ‘/_api/web/GetFolderByServerRelativeUrl’, pass in the server relative path to the folder, and call files/add on that.
- The HTTP verb for the operation should be POST. If you’re actually familiar with REST from elsewhere, you might expect that PUT would work here (and might even be required for ‘overwrite=true’). But if you try PUT, the SharePoint Lists endpoint will return an error saying that PUT is not allowed and you should use POST. Curiously, despite the error return, the operation does succeed and create or overwrite the file. Don’t fight it, just use POST, as ignoring the returned error can only lead to sorrow down the line.
- I send the header saying I accept “application/json;odata=verbose”. Ideally, your production code should use JSON light and “application/json;odata=nometadata”, which makes the SharePoint RESTful web services less chatty. The exception to that is if you’re on SharePoint 2013, the server hasn’t been configured to use JSON light, and you are not a farm admin and cannot convince a farm admin to configure it. JSON light came out after 2013, so Service Pack 1 is required, and changes to the web.config are required, before you can use it. I happen to be in exactly that position as I write the code for this post. Not my farm, service pack 1 is installed, but dozens of web.config files need to be updated, and it would take an act of congress to get that done just so my REST traffic is a little less chatty. No biggie, verbose is better for figuring stuff out an tutorials anyway, and perfectly fine for production if that’s all you have available to you.
- I also set the header “X-RequestDigest” to the request digest from the wiki form. This is required for any write operations. The request digest is only good for 30 minutes, so on a SPA, I should probably refresh it too by calling:This will refresh to form digest if and only if it is needed, but I haven’t actually done that in this code.1UpdateFormDigest(_spPageContextInfo.webServerRelativeUrl, _spFormDigestRefreshInterval);
- And finally I set the “content-length” header to the number of bytes in the buffer, which is binary (ArrayList) here because I’m uploading images.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | var uploading = 0; // construct url to upload a file using REST var url = _spPageContextInfo.webAbsoluteUrl + "/_api/web/lists/getByTitle('" + listTitle + "')" + "/RootFolder/Files/add(url='" + file.name + "',overwrite='true')" + "?" + "@TargetLibrary='" + listTitle + "'&@TargetFileName='" + file.name + "'"; // upload the file ajax({ method: "POST", url: url, headers: { 'accept': 'application/json;odata=verbose', 'X-RequestDigest': document.getElementById("__REQUESTDIGEST").value, 'content-length': buffer.byteLength }, data: buffer, success: function(request) { uploading--; }, error: function(request) { // on failure call decrement uploading--; errorAlert(request); } }); |
REST Read
The read operation is a simple GET and does the following:
- Build the url using the list title. Also, add the request parameter $select, which is set to a comma separated list of internal field names, specifying which fields I want returned.
- Again I set the header accept to “application/json;odata=verbose”, for the same reasons I used this in the create operation. I’m going to stop pointing this one out, because it’s going to be the same throughout. If you specify “odata=minimalmetadata” or “odata=nometadata” the structure of the returned data is different, so you’d need to adjust the code slightly.
- There is a fair amount of code in the success callback, but this is just DOM manipulation code to do things like add a list item for each picture, and preserve the currently selected picture if any.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | // get the UL element that displays the current picture list var pictureList = document.getElementById("picturelist"); // construct the REST url for reading list items var serviceUrl = "/_api/Web/Lists/getByTitle('" + listTitle + "')/Items"; // construct the query params, $select = a list of all field internal names we want var serviceParams = "$select=FileRef,Title,Description,Id,Created,Modified,GUID"; // put them together to form the REST url var url = _spPageContextInfo.webAbsoluteUrl + serviceUrl + "?" + serviceParams; // call the Lists REST service ajax({ method: "GET", url: url, headers: { 'accept': 'application/json;odata=verbose' }, success: function(request) { // json.d.results contains the list of items for (var i = 0; i < request.responseJSON.d.results.length; i++) { // initialize fields I need ensuring I won't have to worry about // null or undefined properties later on var current = request.responseJSON.d.results[i]; current.Name = current.FileRef; current.Name = current.Name.substr(current.Name.lastIndexOf("/") + 1); current.Title = current.Title || ""; current.Description = current.Description || ""; // try to get the LI element for the current item var input = document.getElementById("item" + current.ID); if (input) { // if we found it, just update it's data- attributes var li = input.parentNode; li.setAttribute("data-title", current.Title); li.setAttribute("data-description", current.Description); li.setAttribute("data-etag", current.__metadata.etag.replace(/"/g, "")); } else { // otherwise, append a new LI element for the new item. var container = document.createElement("div"); container.innerHTML = "<li data-id='" + current.ID + "' " + "data-title='" + current.Title + "' " + "data-description='" + current.Description + "' " + "data-etag=" + current.__metadata.etag + ">" + "<input type='radio' name='item' id='item" + current.ID + "' value='" + current.ID + "' />" + current.Name + "</li>"; pictureList.appendChild(container.children[0]); } } // if we got a selected index earlier if (index) { // select it again and initialize the update form controls var current = document.getElementById("item" + index); current.checked = true; var title = current.parentNode.getAttribute("data-title"); var description = current.parentNode.getAttribute("data-description"); document.getElementById("title").value = title; document.getElementById("description").value = description; document.getElementById("updatePanel").style.display = "block"; } else { // otherwise, hide the update form and blank it's controls document.getElementById("updatePanel").style.display = "none"; document.getElementById("title").value = ""; document.getElementById("description").value = ""; } }, error: function(request) { errorAlert(request); } }); |
REST Update
The update operation is probably the most complicated. First, I need to specify the data as a JSON string with the following format:
1 2 3 4 5 6 7 8 | var changes = { '__metadata': { 'type': listItemEntityTypeFullName }, 'Title': document.getElementById("title").value, 'Description': document.getElementById("description").value }; changes = JSON.stringify(changes); |
The metadata is only required because I’m using “odata=verbose”. It contains a single property, which is set to the ListItemEntityTypeFullName of the list, which I can get from the Lists endpoint. I do that with the following code (Note: I specifically set $select to ListItemEntityTypeFullName, otherwise it will be deferred, i.e. not available to me):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // this is only required because we're using odata=verbose var listItemEntityTypeFullName = undefined; if (typeof(listItemEntityTypeFullName) === "undefined") { // we only want to get this once, it doesn't change, but it is unique // for each list, consturct the url for the Lists REST service, you // must select ListItemEntityTypeFullName or it will be deferred var url = _spPageContextInfo.webAbsoluteUrl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')?$select=ListItemEntityTypeFullName"; // call the REST service ajax({ url: url, headers: { 'accept': 'application/json;odata=verbose' }, success: function(request) { // on success, store the list-specific entity type in a global for // use in update calls listItemEntityTypeFullName = request.responseJSON.d.ListItemEntityTypeFullName; }, error: function(sender, args) { errorAlert(request); } }); } |
The returned entity type from this call is “SP.Data.PicturesItem”. At this point you might be tempted to say “I’m only working with pictures, I can just hard code this.” Not so. The list entity type is unique to the list. So if I had a second picture list in the site, it would be something like “SP.Data.PicturesItem2”. So if you want your code to work with any picture list, you need to retrieve this programmatically.
Also, this code is asynchronous, and so is the update method. So I either need to do nested asynchronous calls or get this on startup. Since I only need to get this once, rather than for each update, I get it on startup by wrapping it in some initialization code like so (the function just populates the global listItemEntityTypeFullName):
1 2 3 4 5 | // don't do anything until sp.js is loaded ExecuteOrDelayUntilScriptLoaded(function() { getListItemEntityTypeFullName(); // prime the pump readImages(); }, "sp.js"); |
Now that I have the list entity type, I’m ready to talk about performing an update. The code is below, and it does the following:
- Constructs the JSON payload in the format described above.
- The HTTP verb is a MERGE operation.
- Set the ‘content-Type’ header to ‘application/json;odata=verbose’. The accept header tells the service what kind of data I want back. The content type header tells the service what kind of data I’m passing in. I didn’t need to set this for the binary picture create operation, but I’m passing a JSON encoded object here and I need to tell the service that.
- Set the ‘X-RequestDigest’ header as described above (I’m going to stop calling this out too, it’s the same for all write operations).
- Set the header ‘X-HTTP-Method’ to MERGE; like I said earlier there’s a fair amount of redundancy in the REST API.
- Set the eTag header. This is basically a 1-up version number. It’s purpose is that if I set this to “1” on a merge, and SharePoint sees the current value is “3”, it’s going to give me one of those “Somebody else seems to have updated the item” errors, and say please refresh the page. If you want it to write regardless of eTag, you can pass “*” as the eTag. Also, it’s a bit quirky, but the etag isn’t a number, it’s a numeric string. By that I mean that the value you get for it is a number in double quotes. When I first shoved that into a data- property I ended up with data-etag=””3″”, which isn’t valid HTML. I could get around that by using single quotes, but that’s pretty ugly. I chose to strip out the quotes and add them back when I set the header, which is why my header value is ‘”‘ + etag + ‘”‘. If you forget these quotes, you will get an error every time. Finally, for the read operation above, the etag was only returned as part of the metadata. So if I use “odata=nometadata” I assume I don’t get this. So if I really want to use etags to avoid overwriting a newer version, that might be an argument that sometimes “odata=verbose” makes sense even for production code in an environment where JSON light is available.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | // get the etag var li = document.querySelector("li[data-id='" + id + "']"); var etag = li.getAttribute("data-etag"); // construct the merge object with a property for each field we // want to update var changes = { '__metadata': { 'type': listItemEntityTypeFullName }, 'Title': document.getElementById("title").value, 'Description': document.getElementById("description").value }; changes = JSON.stringify(changes); // construct the REST service url var serviceUrl = "/_api/Web/Lists/getByTitle('" + listTitle + "')/Items(" + id + ")"; var url = _spPageContextInfo.webAbsoluteUrl + serviceUrl; // call the REST service as a MERGE ajax({ method: "MERGE", url: url, headers: { 'accept': 'application/json;odata=verbose', 'content-Type': 'application/json;odata=verbose', 'X-RequestDigest': document.getElementById("__REQUESTDIGEST").value, 'X-HTTP-Method': 'MERGE', 'IF-MATCH': '"' + etag + '"' }, data: changes, success: function(request) { // on success, reload the images list and pop up an alert readImages(); alert("Update successful!"); }, error: function(request) { errorAlert(request); } }); |
Easy peasy, right?
REST Delete
And last but not least, the delete operation. There isn’t much new here, except:
- This is the only operation that doesn’t use the Lists endpoint. The path is ‘/_api/web/getfilebyserverrelativeurl’, and it gets passed the server relative path to the file.
- The HTTP verb and the ‘X-HTTP-Method’ header are both set to DELETE.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // get the picture name var name = document.querySelector("li[data-id='" + id + "']").textContent; // construct the url for the REST service var base = (_spPageContextInfo.webServerRelativeUrl === "/" ? "" : _spPageContextInfo.webServerRelativeUrl); var serviceUrl = "/_api/web/getfilebyserverrelativeurl(" + base + "/" + listTitle + "/" + name + "')"; var url = _spPageContextInfo.webAbsoluteUrl + serviceUrl; // call the rest service ajax({ method: "DELETE", url: url, headers: { 'X-RequestDigest': document.getElementById("__REQUESTDIGEST").value, 'X-HTTP-Method': 'DELETE' }, success: function(request) { // delete the LI element for the now deleted picture var current = document.getElementById("item" + id); current.parentNode.parentNode.removeChild(current.parentNode); // hide the update panel, since nothing is selected now document.getElementById("updatePanel").style.display = "none"; // reload the UL of images readImages(); }, error: function(request) { errorAlert(request); } }); |
That’s it for basic CRUD operations on SharePoint documents. In my next post, I’ll redo the ugly SPA using fetch, which will get me back to using promises. The complete source code for the ugly SPA is attached. Just make sure you have a picture library with Pictures as the title, or change the listTitle variable defined in the code. Then drop it in a document library and click on it to play around with it.