Blog

SPServices SharePoint Attachments in Internet Explorer 9

A little over eight months ago I wrote a very brief post about using SPServices to add attachments to a SharePoint list. Full credit here goes to Brendan Wilbore who wrote the blog post that I linked to.

There was a problem, though ā€“ the solution relies on the fileReader JavaScript feature which requires Internet Explorer 10, and the default browser deployed within my organization is Internet Explorer 9. What we need is a fileReader alternative for older browsers. Thankfully, such a thing exists. Today Iā€™m going to post some example code that uses the fileReader polyfill and works in older browsers.

What You Need

The code has several pre-requisites. Youā€™ll need jQuery, jQuery UI, SPServices, SWFObject and the JavaScript and flash file that form the fileReader polyfill.

For the purposes of my demo I created a simple SharePoint list called ā€œFile Attachment Test.ā€ The list has a single field ā€“ title ā€“ and attachments to the list are enabled. Your list is probably named differently, so youā€™ll need to change the references in the code to reflect your list name.

The Code

<html>
<head>
   <meta charset="utf-8" />
   <title>File Attachment Test</title>
   http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js
   http://ajax.googleapis.com/ajax/libs/jqueryui/1.10.4/jquery-ui.min.js
   http://ajax.googleapis.com/ajax/libs/swfobject/2.2/swfobject.js
   http://js/jquery.FileReader.min.js
   http://js/jquery.SPServices-2013.01.min.js
   
      var selectedfile = false;

      $(document).ready(function() {
         $('input#itemfile').fileReader({filereader: 'js/filereader.swf'});

         $('input#itemfile').change(function(e) {
            selectedfile = e.target.files[0];

            $('span#filename').html(selectedfile.name);
            $('span#fileinput').hide();
         });

         $('input#createitem').click(function() {
            $().SPServices({
               operation: 'UpdateListItems',
               async: false,
               listName: 'File Attachment Test',
               batchCmd: 'New',
               webURL: '/demo',
               valuepairs: [
                  ['Title', $('input#itemtitle').val()]
               ],
               completefunc: function(xData, Status) {
                  if (Status == 'success' && $(xData.responseXML).find('ErrorCode').text() == '0x00000000') {
                     currentitem = $(xData.responseXML).SPFilterNode("z:row").attr("ows_ID");
                     alert('List item created with ID ' + currentitem);

                     if (selectedfile) {
                        filereader = new FileReader();
                        filereader.filename = selectedfile.name;

                        filereader.onload = function() {
                           data = filereader.result;
                           n = data.indexOf(';base64,') + 8;
                           data = data.substring(n);

                           $().SPServices({
                              operation: 'AddAttachment',
                              async: false,
                              listName: 'File Attachment Test',
                              listItemID: currentitem,
                              fileName: selectedfile.name,
                              attachment: data,
                              completefunc: function(xData, Status) {
                                 alert('File uploaded');
                              }
                           });
                        };

                        filereader.onabort = function() {
                           alert('Upload aborted');
                        };

                        filereader.onerror = function() {
                           alert('Upload error');
                        };

                        filereader.readAsDataURL(selectedfile);
                     }
                  } else alert('List item creation failed');
               }
            })
         });
      });
   
</head>
<body>
   <p>Title:<br><input type="text" id="itemtitle"></p>
   <p>File:<br><span id="fileinput"><input type="file" id="itemfile"></span><span id="filename"></span></p>
   <p><input type="button" id="createitem" value="Go!"></p>
</body>
</html>

Notes

The fileReader polyfill takes the file input box and puts the flash file on top of it, so that the file selection and upload is handled by flash instead of natively in the browser. I found that this fell apart of the file input box didnā€™t remain in the same place on the page. In other words, I had problems if I tried to use jQueryā€™s .show() and .hide() functions (or similar).

I solved this by putting the file selection form in a pop-up window. If the page you place your form on is static (i.e. nothing changes after the DOM is loaded) then you shouldnā€™t have this problem.

Enjoy!

Blog

SharePoint Development: Lesson 4

Welcome back to my ongoing series on SharePoint development!

I promised last week that Iā€™d follow up lesson three within a week, so here we are! In that previous lesson everything we did was in relation to setting the stage for what weā€™re going to tackle today, so Iā€™ve got no doubt that youā€™ve been waiting with barely contained anticipation to get back to writing some code.

Because all the steps from last week were fairly standard SharePoint stuff, if youā€™re pretty familiar with the platform you may well have skimmed through it. Thatā€™s fine, but for everything we do today to be successful weā€™re going to need to make sure that:

  • Weā€™ve created and populated a list of shipping prices, with different shipping profiles (in our example, these different profiles are based on destination).
  • Uploaded the SPServices library minified javascript to our ā€œwebā€ document library.
  • Located and noted the GUID of the shipping prices list, so that we can programmatically reference it in our code.

With those steps done everything is in place, so letā€™s not waste any more time!

The HTML

The HTML portion of our code hasnā€™t changed very much since we wrote it way back in lesson one, and it doesnā€™t change much today either. That being said, we do have an additional option that forms part of our calculation this time around, so weā€™ll add a form element for that.

Destination province:
Please wait, loading data...

Item weight:
lbs

Shipping cost: $0

As you can see, weā€™ve added a drop-down menu that allows for the selection of the destination province. In the HTML it has a single entry in the list that says ā€œPlease wait, loading dataā€¦ā€ As you might anticipate, weā€™re going to remove this option later on and replace it with the actual choices, but itā€™s probably good practice to have something there because the SPServices library is going to read from our list with an AJAX HTTP request ā€“ in other words the page will load first (with our ā€œplease waitā€ message), and the data to populate the list will be loaded afterwards. Hopefully it will all happen so fast that nobody ever really sees the ā€œloadingā€ message, but you never know.

The JavaScript

OK, now weā€™re really going to start getting into some changes in functionality! First things first, though. We need to include the external libraries weā€™re going to be using. jQuery has been there from the start of our journey, but SPServices is new for today.

/web/js/jquery-1.11.0.min.js
/web/js/jquery.SPServices-2014.01.min.js

The other thing Iā€™m going to do at the beginning of my main block is declare a global variable in which to store list data. There are probably better approaches than this, but for the sake of keeping my code relatively simple this is one Iā€™m taking.

var shippingData = false;

Loading Data from our SharePoint List

Next is where the SPServices magic happens. Immediately inside our jQuery document ready function weā€™re going to call SPServices and grab the data from our list, putting the result of that request inside our shippingData variable. SPServices has many available options that get passed as an object to its main function and more details can be found in the documentation on their website. Like I said though, Iā€™m keeping things simple:

$().SPServices({
   operation: 'GetListItems',
   listName: '{4883AC18-E2A5-4EAF-8446-23B15B43861A}',
   completefunc: function(xData, Status) {
      if (Status == 'success' && ($(xData.responseXML).find('ErrorCode').text() == '0x00000000' || $(xData.responseXML).find('ErrorCode').text() == '')) {
         shippingData = $(xData.responseXML).SPFilterNode('z:row');
         populateDropdown();
      } else alert('Something went wrong!');
   }
});

Not bad, eh? Ten lines of code and weā€™ve read all the data from our SharePoint list. Letā€™s look at what weā€™ve done in a bit more detail.

The code weā€™ve written can be summarized as:

$().SPServices(obj);

All weā€™re doing is calling the SPServices plugin and passing in a javascript object that contains all the options it needs to understand what we want it to do. There are many options we could pass to it, and youā€™ll find more detailed documentation on the SPServices homepage. Iā€™ve kept things as simple as possible and passed in the bare minimum.

Operation: ā€˜GetListItemsā€™

In our example weā€™re dealing with items in a list. GetListItems is the operation we need to read data from a SharePoint list into our webapp. There are many other types of operation related to lists that SPServices could do for us ā€“ creating entirely new lists, adding or removing list fields, deleting lists, etc. Essentially almost anything you could do manually on SharePoint could be done programmatically with SPServices. If we wanted to write data back to a list then UpdateListItems would be the operation weā€™d use.

ListName: '{4883AC18-E2A5-4EAF-8446-23B15B43861A}'

The ListName parameter could take one of several formats. A simple string containing the name of the list will work, but to avoid any kind of confusion between similarly named lists my preference is to pass in the GUID of the list. Remember that the GUID of your list will be different to mine. SPServices can also take a WebURL parameter that tells it which site in your SharePoint collection the list can be found on, but since weā€™re using a GUID thatā€™s unique across all sites in the collection we donā€™t need that.

completefunc:Ā  function(xData, Status)

This is where the real magic happens. The completefunc parameter represents a callback function that SPServices executes once data has been loaded, and it takes two parameters: xData and Status.

Our completefunc does some basic error handling, and then calls another function to do the dirty work.

if (Status == 'success' && ($(xData.responseXML).find('ErrorCode').text() == '0x00000000' || $(xData.responseXML).find('ErrorCode').text() == ''))ā€¦

For the Status parameter thatā€™s passed in to completefunc, weā€™re looking for it to contain a value of ā€˜successā€™. We have to be a little careful about exactly what this means though: To SPServices success means that itā€™s passed a query to SharePoint and received a response. It doesnā€™t mean that our query was well formed, or that we received any useful data back. Basically youā€™ll always get a successful status unless SharePoint is down ā€“ in which case our webapp probably wouldnā€™t be available to users anyway.

To check that our query has truly been executed successfully by SharePoint, we look in the responseXML for an error code. Depending on the version of SharePoint weā€™re running, that will either be 0x00000000 or blank.

shippingData = $(xData.responseXML).SPFilterNode('z:row');

There are few ways we could approach what happens next. My goal was to keep my code as simple as possible so Iā€™m doing some things that may not be best practice. This is one of them: we take the data SPServices has returned and put it into a global variable.

The data SharePoint has passed back to us is in XML format and contains a wealth of information about our query, metadata about the response, and so on. We donā€™t really need any of this stuff ā€“ we just want the data itself ā€“ so SPServices has a function called SPFIlterNode that helps us filter the returned data down to what we actually care about. Weā€™re filtering here by z:row. Each z:row returned represents one entry from our SharePoint list.

populateDropdown();

Now that we have our data in a globally accessible variable, Iā€™m outsourcing the processing of it to another function: populateDropdown. That last step for our completefunc is to call this function.

Populating the Dropdown List

OK! So now we have the data we need loaded from our SharePoint list and resident in memory (in our shippingData global variable) so that we can manipulate it and our users can interact with it. The first step of this process is to populate the relevant options into the select box we created in our HTML. Iā€™m doing that (surprise surprise) with the populateDropdown function.

function populateDropdown() {
   $('select#destination option').remove();

   for (i = 0; i ' + $(shippingData[i]).attr('ows_Title') + '');
   }
}

If you recall, the dropdown list initially contains a single option with the text Please wait, loading dataā€¦. At this point in the story our data is loaded, so letā€™s get rid of that option first:

$('select#destination option').remove();

Done! Good. The next step is to loop through each of the items weā€™ve loaded into our shippingData variable, and add them as an option in the dropdown. We do this with a standard for loop:

for (i = 0; i 

If youā€™re not familiar, this construct sets a variable i to zero, then loops through the following block of code multiple times, as long as i is less than shippingData.length, which is the number of items in our shippingData variable. On each iteration i is incremented by one (i++).

On each loop we add an item to our dropdown, using jQuery to append the relevant HTML. Based on the data that exists within our SharePoint list, we end up with these options in our dropdown:

Each $(shippingData[n]) has an attribute for each of the columns in our list. These attributes all have the prefix ows_, and, as mentioned in lesson 3, they are all referenced by the original name of that column (even if itā€™s been renamed since it was created). Thatā€™s why weā€™re using the attribute ows_Title to get at the data thatā€™s in our Province column: the column was called Title when the list was created, and we renamed it.

Performing the Calculation

With any luck everything in our story up to this point will have happened in a split second, but regardless weā€™re now ready for user input. The user selects the destination province, inputs the weight of the item being shipped, and hits the calculate button.

The calculation itself is really no different from the one we built in lesson 1, the difference being that our variables are defined by the list data rather than hardcoded in.

We still identify that the Calculate button has been pressed through the use of the jQuery $(ā€˜input#calculateā€™).click(function() {});, but our first step is now to set some variables based on whatever is selected in the destination province dropdown at the time.

bp = parseFloat($(shippingData[$('select#destination').val()]).attr('ows_BasePrice'));
bw = parseInt($(shippingData[$('select#destination').val()]).attr('ows_BaseWeight'));
ap = parseFloat($(shippingData[$('select#destination').val()]).attr('ows_AdditionalPrice'));
aw = parseInt($(shippingData[$('select#destination').val()]).attr('ows_AdditionalWeight'));

We set each of these variables by reading the relevant attribute (representing the column in our SharePoint list) from $(shippingData[n]), where n is the value of the destination dropdown, $('select#destinationā€™).val(). After that, itā€™s business as usual:

var shippingcost = bp;

if ($('input#itemweight').val() > bw) {
   shippingcost += (Math.ceil(($('input#itemweight').val() - bw) / aw) * ap);
}

$('span#shippingcost').html(shippingcost);

Putting it All Together

And weā€™re done! The completed code block ā€“ including all the javascript and HTML ā€“ that we copy and paste into our content editor webpart is as follows:

/web/js/jquery-1.11.0.min.js
/web/js/jquery.SPServices-2014.01.min.js

   var shippingData = false;

   $(document).ready(function() {
      $().SPServices({
         operation: 'GetListItems',
         listName: '{4883AC18-E2A5-4EAF-8446-23B15B43861A}',
         completefunc: function(xData, Status) {
            if (Status == 'success' && ($(xData.responseXML).find('ErrorCode').text() == '0x00000000' || $(xData.responseXML).find('ErrorCode').text() == '')) {
               shippingData = $(xData.responseXML).SPFilterNode('z:row');
               populateDropdown();
            } else alert('Something went wrong!');
         }
      });

      $('input#calculate').click(function() {
         bp = parseFloat($(shippingData[$('select#destination').val()]).attr('ows_BasePrice'));
         bw = parseInt($(shippingData[$('select#destination').val()]).attr('ows_BaseWeight'));
         ap = parseFloat($(shippingData[$('select#destination').val()]).attr('ows_AdditionalPrice'));
         aw = parseInt($(shippingData[$('select#destination').val()]).attr('ows_AdditionalWeight'));

         var shippingcost = bp;

         if ($('input#itemweight').val() > bw) {
            shippingcost += (Math.ceil(($('input#itemweight').val() - bw) / aw) * ap);
         }

         $('span#shippingcost').html(shippingcost);
      });
   });

   function populateDropdown() {
      $('select#destination option').remove();

      for (i = 0; i ' + $(shippingData[i]).attr('ows_Title') + '');
      }
   }


Destination province:
Please wait, loading data...

Item weight:
lbs

Shipping cost: $0

Taking it Further

The next steps with our shipping calculator app would be to add some additional error-checking and handling, and maybe amend the code to avoid using unnecessary global variables. Iā€™ve kept things as simple as possible here for the sake of example.

After that? As I mentioned earlier, thereā€™s a lot of cool stuff we can do with SPServices. Where you go from here is really up to you, but hopefully you can see some possibilities. Even with the basic building blocks of reading from and writing to lists, itā€™s possible to build some really cool stuff on top of SharePoint, possibly even taking the approach of using SharePoint as a database for a webapp that has its own look and feel.

Enjoy!

Blog

SPServices addAttachment jQuery Example

Update: I’ve posted some example code that works in Internet Explorer 9!

If you’re having a few issues adding attachments via ajax and SPServices on SharePoint have a look over the code snippets below.

To upload a file to a list you need to make use of the fileReader javascript class, using the readAsDataURL method and stripping the first part off the dataurl to get the base64 component. Then submit this to SPServices.

I’ve been asked a few times to add the ability to upload attachments to SharePoint tools that I’ve created, and I’ve never been able to achieve it until I eventually came across this blog post last week.

If (like me) you’re developing in a front-end only way without any server-side programming then it seems like this is the way to upload files and attach them to SharePoint list items.

It relies on the javascript fileReader feature so your users will need a fairly modern browser… which is where I ran into trouble. The default browser deployed within my company is Internet Explorer 9, and that doesn’t have fileReader support.

With much work and even more googling I was able to get this technique to work in Internet Explorer 9. In the future I’ll write more about how I managed it, and how you can too!

SPServices addAttachment jQuery Example

Blog

Le Projet de 20% est Mort, Vive le Projet de 20%

Traditional proclemations aside, you may have read an article or two earlier in the year saying that Google has killed it’s “20% time” policy.

If you’re unfamiliar and you don’t feel much like clicking the above link, 20% time is…

“…a well-known part of our philosophy here [at Google], enabling engineers to spend one day a week working on projects that aren’t necessarily in our job descriptions.”

It’s well publicised that 20% time has been a significant contributing factor in giving us some of the Google products that we know and love today, so I hope for that reason that these rumors aren’t true. Regardless of that though, 20% time does seem at odds with the way the modern world works.

I’ve blogged a couple of times about ROWE so I’ll do my best not to digress into a further soliloquy about its merits here, but suffice to say I don’t measure my work in terms of time anymore. Allotting 20% of my time to projects that aren’t necessarily in my job description would be nearly impossible for me – not necesarilly because my organization wouldn’t allow it, but because I don’t know that I could figure out what 20% of my time is.

I’ve never worked somewhere with a policy of 20% time similar to Google, but that’s irrelevant. ROWE offers me something much better. I’m essentially free to use my time however I wish as long as the work gets done, and for me “20% projects” areĀ absolutely a part of that.

Just because these 20% projects (as I’ll keep referring to them) aren’t right out of my job description doesn’t mean they aren’t work related, but they do offer me the freedom to try new things without fear of failure, and that leads to some great innovation (it leads to some failures and dead ends too, but that’s the point – it doesn’t matter).

I plan to blog some more about the gritty technical details in the not too distant future, but I’ve recently built a dashboard web-app on top of SharePoint using jQuery and SPServices. People love it, and it’s greatly increased my stock at work over the last few days. If my boss had come to me and asked me in a formal setting to develop this I don’t know if I’d have touched it. If it were a formal project from the start we’d have had to get IT to build it for us, probably at great expense. I, by contrast,Ā learned how to do this stuff as I went along, and when I started work on it I had no idea if I’d be able to bring things to a successful conclusion.

My hope for my organization as ROWE becomes more of a popularised concept and way of working is that it encourages and enables other people to try new things – filter out the noise, spend less time doing and more time thinking. It worked for me, and it can work for you too.