How to make a SharePoint List work with Bootstrap – using SPServices and jQuery

For those of you who have worked with SharePoint in the past, particularly with lists, CEWPs (Content Editor Web Part) and front end code, then you probably know that these lists do not work well in mobile. The markup is just too messy – invalid tags, divs inside spans, inline Javascripts, inline styles and even tables! So it’s no surprise that making SharePoint work with Bootstrap is not an easy task.

Note that this plugin will require the following files:
  • jquery.min.js
  • jquery.SPServices-2014.02.min.js
  • bootstrap.min.css

In this post, I will show you how to use a Javascript plugin that I’ve written to basically recreate the markup of these SP lists so it works with Bootstrap / Mobile. It also has extra functionality like paging, filters, expand/collapse and other options. By the time we’re done, your list will look real good in desktop, tablet and mobile view:
View in Github

How to use the Plugin:

Note that I’ve only tested this with SharePoint 2013. You may need administrator rights to your website, as well as you should know how to work with custom lists and document libraries and such.
After you have downloaded the files, open the “sp-bootstrap-list-viewer.html” in you text editor. This is where you will add the plugin information.
You have to change the CHANGE-TO-YOUR-PATH in the JavaScript and CSS inclusion – to your downloaded path.
Then, you add 2 things: the list name, the “Answer” column. The rest of the options are optional such as the “filterBy” and “rowLimit”. The table below will show you want the rest of the options do.
sp-list1
Once you have edited the html file, upload it to a location in your SharePoint site, typically a document library. You will need to copy the location of the file.
sp-list1
Let’s go ahead and add a CEWP into your SharePoint page. in the “Content Link” input, add the location of the html file you’ve just uploaded.
sp-list1
Click “OK” and if everything is configured correctly, you should see the plugin do its thing:
sp-bootstrap-list-viewer
The table below contains the options for the plugin and an explanation of what they’re for:

Option name Default Value Required? Description
instance Random Value Yes Used as the “Id” of your plugin
listName none Yes Name of the SP list
question Title Yes Title of the row
answer none Yes Body of the row
filterBy none No Category column to filter rows
rowLimit 5 No Number of rows to show

One thing to remember. If you plan to use the plugin multiple times in a page, you need to change the “instance” value to a random number prefixed with alphanumeric characters. You also need to make this the “Object” name of your html (where it says var spBL233565656 = new spBootstrapList …). This will make the instances of your plugin unique in every page and will not clash with each other.

Under the hood:

For those of you who want to know more about how the plugin works, I will show you some of it’s main functionality. Note that this was originally built for a FAQ list for SharePoint, hence the FAQ names such as “Question” and “Answer”. Though the plugin will work with any kind of SP list.

Requirements

We will need some help with Mark Anderson’s SPServices – a jQuery plugin that basically does the web service calls for us. This allows us to work with the lists’ data and convert it to our desired markup. Of course we will need Bootstrap – which will take care of the responsive and overall styles, as well as jQuery for DOM manipulation.
Notice the required scripts and styles. You can link to them externally or download them to a document library in SharePoint. Our constructor spBootstrapList() calls our Javascript class, passing along our required arguments. Again, the variable name “spBL233565656” and the value for “instance” has to be identical. This is for using it multiple of these in a single page.
So going into the Javascript plugin: sp-bootstrap-list-viewer.js, you will see that on first load there’s a public method that gets called to set things up “init()”. This makes the initial call to the plugin “SPServices”. As I’ve mentioned, the plugin does the AJAX call to the SharePoint web service. It uses SOAP as its protocol so the request / response will be in XML format. Our init() method also checks for valid fields:

var init = function(){
var ajax = methods.callSPServices(_settings.rowLimit, true);
$(ajax.responseXML).SPFilterNode("z:row").each(function(e) {
    var errors = Array();
    if(typeof $(this).attr('ows_' + _settings.question) == 'undefined'){
        errors.push("Invalid field name for Question: " + _settings.question);
    }
    if(typeof $(this).attr('ows_' + _settings.answer) == 'undefined'){
        errors.push("Invalid field name for Answer: " + _settings.answer);
    }
    if(_settings.filterBy !== '') {
        if(typeof $(this).attr('ows_' + _settings.filterBy) == 'undefined'){
            errors.push("Invalid field name for FilterBy: " + _settings.filterBy);
        }
    }
    _errors = errors;
    methods.parseResponse($(this));
});

Then we parse our response and we fill our internal object “_faqs”: Notice the SharePoint internal list names that start with “ows_” – these are how it comes out of the SOAP response. We parse it to our own object with more meaningful keys and it’s values:

parseResponse : function(resp){
    var record = {};
    record.question = resp.attr('ows_' + _settings.question);
    record.answer = resp.attr('ows_' + _settings.answer);
    record.created = resp.attr('ows_' + 'Created');
    if(_settings.filterBy !== ''){
        record.filterBy = resp.attr('ows_' + _settings.filterBy);
    }
    _faqs.push(record);
},

By the way, in order for us to make public methods from within our plugin is that we assign it to the “this” keyword. This way it becomes accessible from the outside. Here is an example of all the public methods of our plugin:

this.paginate = paginate;
this.filter = filter;
this.showHideFilters = showHideFilters;
this.clearFilter = clearFilter;
this.showHideAnswer = showHideAnswer;

Continuing with our process, note that we need the “_faq” array to be filled for our next method to fire. This method is called methods.buildFaqs(), So we built another object called “methods” and inside this is a series of functions. This will be our “internal” methods.
If you look at the buildFaqs() method, the code looks like below:

buildFaqs : function(){
  var output = '';
  for(var i=0; i<_faqs.length; i++){
    var hr = (i != (_faqs.length-1)) ? '

[code above has been truncated – due to syntax highlighting error]
So you see, we have our SharePoint list data in our array, and we build the HTML from it.

Pagination

Our pagination logic is quite interesting. We use the Bootstrap pagination HTML, and for each page, we need to do a AJAX call in order to get the next page ID. This is how SharePoint determines where to offset the record when a query is made by using “ListItemCollectionPositionNext” as a parameter.

var buildPagination = function(){
  if(_errors.length > 0) {
      $('#'+ _settings.instance+' .spBLPagerContainer').html('');
      console.log('Exiting buildPagination()');
      return false;
  }
  methods.getTotal(); //inside here we build filters
  if(parseInt(_totalRecords) > parseInt(_settings.rowLimit)) { //yes paginate
      var pages = Math.ceil(_totalRecords / _settings.rowLimit);
      var positions = Array('');
      for (var i=0; i < (pages - 1); i++) { //builds the values required 'ListItemCollectionPositionNext'
          var pos = '';
          if(i == 0){ //skips first to not pass pos
              positions.push(methods.getNextPos(pos));
          }else{
              var offset = i;
              positions.push(methods.getNextPos(positions[offset]));
          }
      }
  // console.log(positions);
  methods.buildPagination(positions);
  }else{ // no need to paginate
      $('#'+ _settings.instance+' .spBLPagerContainer').html('');
  }
}

We then proceed to build the actual markup with our internal method: methods.buildPagination():

buildPagination : function(items){
  if(_firstLoad == false){
      $('#'+ _settings.instance+' .spBLPagerContainer').html(pager);
  }
  _positions = items;
  var pager = '

 
[code above has been truncated – due to syntax highlighting error]
Each button has an inline “onclick” that calls our public “paginate()” method. Each of them also has the attribute “data-position” which holds the offset “ID” that we talked about:

var paginate = function(clicked){
  if($(clicked).parents('li').hasClass('active') || $(clicked).parents('li').hasClass('disabled')){
      return false;
  }
  $('#'+ _settings.instance+' .pagination li').removeClass('active');
  var pos = $(clicked).attr('data-position') != '' ? $(clicked).attr('data-position') : '';
  if($(clicked).hasClass('NextButton')){
      $('#'+ _settings.instance+' a[data-position="'+pos+'"]').not('.NextButton').parents('li').addClass('active');
  }else if($(clicked).hasClass('PrevBtn')){
      $('#'+ _settings.instance+' a[data-position="'+pos+'"]').not('.PrevBtn').parents('li').addClass('active');
  }else{
      $(clicked).parents('li').addClass('active');
  }
  methods.updateNextPrevBtns(pos);
  methods.clearFaqs();
  var options = {
      CAMLViewFields: methods.fieldsToRead(true),
      CAMLQuery: methods.query(),
      CAMLRowLimit : _settings.rowLimit,
      CAMLQueryOptions: ""
  }
  var ajax = $().SPServices(methods.mergeSPCommon(options));
  $(ajax.responseXML).SPFilterNode("z:row").each(function(e) {
      methods.parseResponse($(this));
  });
  methods.buildFaqs();
}

You noticed too from the code above that we use SPServices on each click. That’s because we are only grabbing a predefined set of records – which is the limit that we pass into our plugin.

Filters

So the plugin supports filtering. This means that you can choose a field from your list and make that a “filter” column.
First, assuming you’ve selected a valid field, we have to build it. Take a look at the code below:

getFilterByValues : function(resp){
  var filterList = Array();
  var filterListUnique = Array();
  $(resp.responseXML).SPFilterNode("z:row").each(function(e) {
      var filterValue = $(this).attr('ows_' + _settings.filterBy);
          if(typeof filterValue !== 'undefined'){ //only add to array if theres a value
              filterList.push(filterValue);
          }
  });
  $.each(filterList,function(i,el){ //get rid of duplicates
      if($.inArray(el, filterListUnique) === -1){
          filterListUnique.push(el);
      }
  });
  _filterByListUnique = filterListUnique;
  _filterByList = filterList;
}

The above simply grabs the data. And since there are duplicate entries – we will have to make them unique and fill up our “_filterByListUnique” array. This is next:

buildFilters : function(){
  var count = 0;
  var out = '';
  out +=

[code above has been truncated – due to syntax highlighting error]
The above builds out the checkboxes we need for our filtering capabilities. Now when each item is clicked, it triggers our filter() method:

var filter = function(clicked){
  _firstLoad = false;
  _filterByValues = [];
  $('#'+ _settings.instance+'-filtersArray :checkbox:checked').each(function(e){
      _filterByValues.push($(this).val());
  })
  methods.filter();
}

Which actually calls our internal methods.filter() method:

filter : function(){
  $('#'+ _settings.instance+'-filtersArray input').attr('disabled', 'disabled');
  $('#'+ _settings.instance+' .clearFilter').attr('disabled', 'disabled');
  methods.clearFaqs();
  var ajax = methods.callSPServices(_settings.rowLimit, true);
  $(ajax.responseXML).SPFilterNode("z:row").each(function(e) {
      methods.parseResponse($(this));
  });
  setTimeout(function(){
      methods.buildFaqs();
      buildPagination();
      $('#'+ _settings.instance+'-filtersArray input').removeAttr('disabled');
      $('#'+ _settings.instance+' .clearFilter').removeAttr('disabled');
  },10);
  if(_filterByValues.length > 0){
      $('#'+ _settings.instance + ' .clearFilter').show();
  }else{
      $('#'+ _settings.instance + ' .clearFilter').hide();
  }
}

A lot going on here. You see that we make a new AJAX call through SPServices, passing our filter value. We parse the response, rebuild our items through “methods.buildFaqs()” and rebuild our pagination through buildPagination().
There’s much more logic in the plugin such as the creating the CAML queries – but I decided not to go through them. If you’re well versed with .NET and SharePoint – you will be able to decipher these easy.

Console Debugging

Once you have the code in place – save and refresh your page. If by any reason some of the options are passed incorrectly, error handling is added and will be displayed in the HTML. You also need to turn on the console for more messages of what’s going on. I’ve added a couple of helpers that will get you information about your list such as getAllColumns(). This will display all of the fields that belong to the list you’re working with. Simply use the instance name + getAllColumns().

Conclusion

As you can see, we can make SharePoint internal lists work responsively. We may have to redo the markup entirely – but there are web services in place to grab the data. Thanks to open source code such as SPServices, jQuery and Bootstrap – our task becomes somewhat easier.
Feel free to comment below.

56 Comments

  1. I’m trying to understand the relationship between this method and using JSLink or if they are unrelated / can be combined. I’ve seen that listed as the optimal way to change the rendering of list views. What are your thoughts?

    Reply
      • Curious as to how the Question/Answer plays out in a different Sharepoint Library – have a document library and trying to use this and I can’t seem to win with the Answer field.

        Reply
        • Add a custom column and call it “Answer” for now? Also, make sure it’s named “Answer” as the internal name. SP creates an internal name for the columns.

          Reply
      • Not sure I’m following you still. Sorry couldn’t reply to your response, wouldn’t let me.
        I have a Sharepoint Library, it seems to find the Library of documents. It seems to find library as when I put a random name in the name field it returns and error and when I have the correct name, it doesn’t return an error. In that library I have Name, Topic, Types, etc.. fields. I added an Answer field/column but not I understand how that helps. I tried “answer”: “Answer” or “” (blank) and it returns invalid field name for Answer: Answer. How would I get it to pull all documents within that particular library?

        Reply
  2. First, this is an excellent solution with many good helpers included. Thank you for sharing! I did have one question that is puzzling me. I have a list with many items but only wanted to display those items that are approved to be displayed. I found the query function works great for the filters because the numbers shown when you ShowCategories is correct. However when buildfaqs function executes it will still display all items even if the approved column = false. I tried to add the viewName option for SPservices but that still is not working as expected. Do you have any ideas? Thanks again.

    Reply
    • I am hunching it’s the column name that you cannot find. SharePoint does funny things to the internal names of the fields. In the plugin, it has a function called “this.getAllColumns()” – simply run that in the console. You have to prepend with the object name ie. spBL233565656.getAllColumns();
      Now in the console, search for that column that is giving you problems, see if the internal name matches your query.

      Reply
  3. This seems to work perfect thanks!
    The only issue I seem to have is when I try to filter by a certain field. I change the FilterBy value in the html file, and reload, I get a message saying ” checking for pages… which just wont load.
    Any idea where I could be going wrong?

    Reply
    • It’s most likely that SP has assigned an internal name that is different than what you are adding in the html file. There is a helper function that you can run -> spBL233565656.getAllColumns() – simply replace the “spBL233565656” with your instance id and you should see a list of columns from that list. Look for your column and use that as the filterby.

      Reply
  4. One final question!
    With regards to order, how do you specify which filter by value goes first?
    Right now I think it is done by list length

    Reply
    • I believe the filter values do not have a sort order. So it actually takes the order of which record goes first. Meaning, which list item has the specific filter value entered, will go first.
      I can add a default sort by alpha in the next version.

      Reply
  5. Interesting, learned a lot from this example – thank you!
    Curious though – in Edge it won’t render at all, however in Chrome or Firefox all is good.
    Also, not sure why my bottom previous/1-2-3/next appear as bullets instead of the clean formatted in your example above.

    Reply
  6. Great job!!
    I was wondering if you have away to render images. For me the “answer”, when using an SP Picture column type, only renders the URL and not the image.
    I tried tricking it using the following:
    record.answer = resp.attr(‘ows_’ + _settings.answer).replace(/^.*(\\|\/|\:)/, ‘&#60 img src &#61 &#34 https://sitename/ifc/TCOP/NWemployees/‘).replace(“,”,’&#34 &#62’);
    Result:

    Reply
    • I believe the picture column type returns a string with two values separated by a delimiter. You can do a .split() and get the value you need.

      Reply
  7. Hi Michael. What if the FilterBy column (Category) is a Lookup value? The Category displayes as: “4;#WhateverYouSetAsCategory” … would you know how to get around this easily?
    Thx for Shareing. Nice feature!

    Reply
  8. Hi,
    This is very cool. I was trying this solution but I have an error in console Exiting buildPagination(). I did try adding the getAllColumns as well and when I added its showing as Unrecognized Expression. Please let me know whatI’m doing wrong?

    Reply
    • I had the same issue and managed to identify it was an issue between bootstrap 3 and 4 default css . V4 didn’t seem to support it (as written above) so I found the elements in bootstrap 3.css and copied them into a page specific .css file. That fixed it for me rather than debugging Michael’s code, as I’m really just a hacker..

      Reply
  9. Nice work, cant get it to work tho.
    I have uploaded the css/js/html localy and linked them in html file.
    But when i add the url o content editor it just keep looking for pages “Checking for pages…”
    The list name is Accordion and enterd in the file. I first had the files in “Site assets” and moved them to Pages dir with no result.
    Is there a way to hardcode the search for the list? The way it is now i cant find the list it seems

    Reply
  10. When getting the same error as i got with the Answer art but with categories.
    I have checked so the interanal name is the same, and it is correct ut still no success.
    Do the columns need to be in a specific order?

    Reply
  11. Hi all!
    Great Solution 🙂
    2 Questions because I’m no professional:
    – Is there a way to order by another column?
    – Ist there a way to show the categories by default (expand by default)?
    Would be very happy about a short reply!
    Regards!

    Reply
    • Is there a way to order by another column? – I don’t think that’s supported at the moment.
      a way to show the categories by default (expand by default)? – I don’t think that’s supported at the moment.
      These are great suggestions – and I can definitely add it soon.

      Reply
  12. Hello Michael,
    i need your help regarding this blog: How to make a SharePoint List work with Bootstrap – using SPServices and jQuery
    I have a problem with the design where there is small squares that are shown near the page numbers (Check screenshot: https://imgur.com/a/QNfjbR1 ) and they are not buttons. I tried to remove them by fixing the files and try different browser but it remain the same. please help if you know at least the problem related to what
    Thank you

    Reply
  13. Thanks so much for this elegant solution. Is there a way to add more column info besides the Answer? For example, another column with a person’s name related to the question.
    Am I just updating the CAML query?
    Thanks.

    Reply
  14. Love this solution Thanks very much. suffered many similar issues to above (internal names different to display names and the pagination thing but managed to get it all fixed and looking sweet.
    It’s looking so sweet I am wondering how I can adapt it to show documents in a sharepoint Shared Docs folder, with the document name as the ‘question’ and a hyperlink as the ‘answer’. I am wondering whether your solution works purely on ‘Lists’ or whether there’s a way to adapt it to ‘forms’ which seems to be the way documents are laid out in sharepoint.
    [domain]/spServer/spSiteName/Lists/Frequently%20Asked%20Questions/ versus
    [domain]/spServer/spSiteName/Shared%20Documents/Forms/AllItems.aspx?RootFolder=blah blah blah

    Reply
      • Hi Mike, Thanks for your response.
        I have managed to prove it with a new TestDocument Library, and even the 1st OOB Shared Document library if I want to filter on ‘Modified By’ (Editor). However I cannot get it to work with a custom choice field I created “Topic” or even if I add a pre-existing (choice) field such as “Report Category” (ReportCategory);
        var spBL233565656 = new spBootstrapList ({
        “instance” : “spBL233565656”,
        “filterBy”: “ReportCategory”,
        “answer” : “FileRef”,
        “listName” : “Documents”
        });
        If I flick filterBy to “Editor” it works. If I change the listname to my TestDocument library and use my custom “Topic” field it works, just as you’d imagine.
        Is there something peculiarly unique to the default documents folder? (Unfortunately I have too much content in the current library to migrate documents to my new custom one)

        Reply
  15. I’ve been using this plugin since last November and its been awesome. I really appreciate all the hard work and effort you put into this. I eventually learned how to customize the js, but I’ve been stuck on trying to sort the array. I’ve tried to use the .sort on the _faq array and even slicing the data into a new array. No matter what I do, I can’t figure out how to sort the data. I’m still new to js, can you please give me some pointers or suggestions on how I can accomplish this?

    Reply
    • This is what I tried in the buildFaqs function right before the loop: sorted = _faqs.sort(function(a, b){return a.question – b.question});
      Hope you can shed some light and provide some assistance.

      Reply

Leave a Comment.