Paging through SharePoint 2013 / Office 365 Lists with JavaScript

One of they most basic requirements for customizations in SharePoint 2013 is displaying lists of items.  For example, you might want to have a list of news items in which you control how that list is rendered in the user interface.  There are several ways to do this including search based display templates, CSOM, JSOM, REST, etc.  We have been using all of these approaches in our custom intranets.

One basic requirement we had was to implement a paging system so that end users could click previous, next and randomly seek to any page.

image

Here is how to implement such a mechanism using JavaScript. 

Two Basic Approaches: Fetch Items from a List or Fetch Items from Search

Using the SharePoint JavaScript APIs, there are two basic approaches to obtain a list of items: 1) query the list where the items are stored directly or 2) query the search index to find items. 

Here is an example for fetching items directly from a news list using the JavaScript API.  The query executes a CAML query against a specific news list with a row limit of 2 items per page.

var queryText = “*”;
var rowsPerPage = 2;
var startRow = 1;
var id = “”;
var contentTypeID = “0x0110”;
var category = “”;


var pathField = “Path”;
var titleField = “Title”;
var bodyField = “BodyOWSMTXT”;
var publishedDateField = “PublishedDateOWSDATE”;


var loadNews = function () {
     $(document).ready(function () {
        SP.SOD.executeFunc(‘sp.js’, ‘SP.ClientContext’, function () {
           
                 var ctx = new SP.ClientContext.get_current();
                 var oWeb = ctx.get_web();
                 var oList = ctx.get_web().get_lists().getByTitle(‘Pages’);
                 var viewFields = “<ViewFields><FieldRef Name=’Title’ /></ViewFields>”;
                 var orderBy = “<OrderBy><FieldRef Name=’Created’ /></OrderBy>”;
                 var where = “”;
                
                 category = manageQueryStringParameter(“category”);
                 if (category != “”)
                 {
                     where = “<Query><Where><Eq><FieldRef Name=’News_x0020_Category’ /><Value Type=’TaxonomyFieldTypeMulti’>” + category + “</Value></Eq></Where></Query>”;
                 }
                 var rowLimit = ‘<RowLimit Paged=”TRUE”>’ + rowsPerPage + ‘</RowLimit>’;
                
                 var viewXML = “<View>”+ where + rowLimit + “</View>”;
                
                 var camlQuery = new SP.CamlQuery();
                 camlQuery.set_viewXml(viewXML);


                var collListItem = oList.getItems(camlQuery);
                 ctx.load(collListItem);
                 ctx.executeQueryAsync(onQuerySuccess, onQueryFail);


                function onQuerySuccess() {
                        
                     var listItemInfo = ”;
                     var listItemEnumerator = collListItem.getEnumerator();
                     var firstPageID = null;
                     var lastPageID = null;
                    
                     $(‘#list’).append(“<ul>”);
                            
                     while (listItemEnumerator.moveNext()) {
                        
                         var oListItem = listItemEnumerator.get_current();


                        // set firstPage ID for the item found
                         if (firstPageID == null)
                             firstPageID = oListItem.get_id();
                            
                         listItemInfo = ‘\nID: ‘ + oListItem.get_id() + ‘\nTitle: ‘ + oListItem.get_item(‘Title’);
                         $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                        
                         // set lastPageID for last possible item
                         lastPageID = oListItem.get_id();
                     }
                    
                        $(‘#list’).append(“</ul>”);
                       
                    }


                function onQueryFail(sender, args) {
                     alert(‘Query failed. Error:’ + args.get_message());
                 }


            });
         });
};
loadNews();


// pull parameters from query field
function manageQueryStringParameter(paramToRetrieve) {
     var queryValue = “”;


    if (document.URL.indexOf(“?”, 0) > 0) {
         var params = document.URL.split(“?”)[1].split(“&”);
         var strParams = “”;
         for (var i = 0; i < params.length; i = i + 1) {
             var singleParam = params[i].split(“=”);
             if (singleParam[0] == paramToRetrieve) {
                 queryValue = singleParam[1];
             }
         }


    }
     return queryValue;
}

Here is an example of the same query but instead of querying the list, we query the search index instead.

var queryText = “*”;
var rowsPerPage = 2;
var startRow = 1;
var id = “”;
var contentTypeID = “0x010100C568DB52D9D0A14D9B2FDCC96666E9F2007948130EC3DB064584E219954237AF3900242457EFB8B24247815D688C526CD44D008B00E60AACCFA944AC4F0B4704E594A4”;
var category = “NewsCategoryChoiceOWSCHCM”;


var titleField = “Title”;
var publishedDateField = “ArticleStartDate”;
var categoryField = “NewsCategoryChoice”;
var category = “”;
var categoryURLParameter = “category”;


var loadNews = function () {
     $(document).ready(function () {
         SP.SOD.executeFunc(‘sp.js’, ‘SP.ClientContext’, function () {
             SP.SOD.executeFunc(“SP.Search.js”, “Microsoft.SharePoint.Client.Search.Query.KeywordQuery”, function () {
           
                 var ctx = new SP.ClientContext.get_current();
                 var oWeb = ctx.get_web();
                 var contextSite = ctx.get_site();
                 var keywordQuery = new Microsoft.SharePoint.Client.Search.Query.KeywordQuery(ctx);
                
                 keywordQuery.set_queryText(“*”);
                 keywordQuery.set_trimDuplicates(true);
                 keywordQuery.set_rowLimit(rowsPerPage);
                
                 var querytemplate = “ContentTypeId:'” + contentTypeID + “*’ “;
                
                 category = manageQueryStringParameter(categoryURLParameter );
                 if (category != “”) {
                     querytemplate += categoryField + ‘:”‘ + category + ‘” ‘;
                 }
                
                 keywordQuery.set_queryTemplate(querytemplate);
                
                 var properties = keywordQuery.get_selectProperties();
                 properties.add(publishedDateField);
                 properties.add(titleField);
                 properties.add(categoryField);


                var searchExecutor = new Microsoft.SharePoint.Client.Search.Query.SearchExecutor(ctx);
                 var results = searchExecutor.executeQuery(keywordQuery);
                 ctx.executeQueryAsync(onQuerySuccess, onQueryFail);


                function onQuerySuccess() {
                        
                     $(‘#list’).append(“<ul>”);
                                                    
                     for (i = 0; i < results.m_value.ResultTables[0].ResultRows.length; i++)
                     {
                         var row = results.m_value.ResultTables[0].ResultRows[i];
                         listItemInfo = ‘\nTitle: ‘ + row[titleField];
                         $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                     }
                    
                        $(‘#list’).append(“</ul>”);


                     
                    }


                function onQueryFail(sender, args) {
                     alert(‘Query failed. Error:’ + args.get_message());
                 }


             });
         });
     });
};


loadNews();


// pull parameters from query field
function manageQueryStringParameter(paramToRetrieve) {
     var queryValue = “”;


    if (document.URL.indexOf(“?”, 0) > 0) {
         var params = document.URL.split(“?”)[1].split(“&”);
         var strParams = “”;
         for (var i = 0; i < params.length; i = i + 1) {
             var singleParam = params[i].split(“=”);
             if (singleParam[0] == paramToRetrieve) {
                 queryValue = singleParam[1];
             }
         }


    }
     return queryValue;
}

As you can see by the differences in the function, the basic approach is similar but in this case we execute a query using the SharePoint search APIs instead of the list APIs. 

Implementing Previous and Next using SharePoint List Query

It is possible to implement a previous and next paging approach using our list query approach.  The approach requires the following code to be added to our CAML query creation above:

var pagingInfo = ‘Paged=TRUE&p_ID=0’;
var page = manageQueryStringParameter(“p_ID”);
var pagedPrev = manageQueryStringParameter(“PagedPrev”);
if (page != “”)
{
     pagingInfo = ‘Paged=TRUE&p_ID=’ + page;
}
if (pagedPrev == “TRUE”)
{
     pagingInfo = ‘Paged=TRUE&p_ID=’ + page + ‘&PagedPrev=TRUE’;
}

var position = new SP.ListItemCollectionPosition();
position.set_pagingInfo(pagingInfo);

camlQuery.set_listItemCollectionPosition(position);

This code retrieves an ID value of either the last displayed item in the case of Next or the first display item in the case of Prev in order to tell SharePoint where to count backwards or forwards.  In addition, you have to set the pagingInfo to include a PagePrev=True attribute for when your user has clicked the previous button. 

The id value is provided when you iterate through your query’s results.  The basic code I have tracks the first item’s ID and the last item’s ID and appends these parameters to the p_ID parameter to pass into the query string:

while (listItemEnumerator.moveNext()) {
                        
     var oListItem = listItemEnumerator.get_current();


    // set firstPage ID for the item found
     if (firstPageID == null)
         firstPageID = oListItem.get_id();
                            
     listItemInfo = ‘\nID: ‘ + oListItem.get_id() + ‘\nTitle: ‘ + oListItem.get_item(‘Title’);
     $(‘#list’).append(“<li>” + listItemInfo + “</li>” );
                        
     // set lastPageID for last possible item
     lastPageID = oListItem.get_id();
}
                    
$(‘#list’).append(“</ul>”);
                       
$(‘#pages’).append(“<a href=’./news-list?category=” + category + “&p_ID=” + lastPageID + “‘>Next</a>”);
$(‘#pages’).append(“<a href=’./news-list?category=” + category + “&p_ID=” + firstPageID + “&PagedPrev=TRUE’>Prev</a>”);

This works well for previous and next.  However, there is no easy method to implement the random access to specific pages, e.g. by click on page 3 to go directly to page 3.  Previous and Next work relative to the current position in the list but the API doesn’t provide the total number of items in the list that we could use to calculate how many pages we need.  In addition, the IDs are not necessarily in sequential order which we would need to figure out what the ID to specify to go to a specific page. 

Implementing Pages Using SharePoint Search

The SharePoint Search API allows us to implement pages because of two important features:

  • The SharePoint Search API provides a value for the total number of items in the query even when the Row Limit is specified.  For example, your news list might have 200 items in it but you only want to display five items per page.  Unlike the SharePoint List API, the Search API provides the value of 200 in the TotalRows property of the table of results.
  • The SharePoint Search API provides a method called set_startRow which allows you to specify the starting position of the results provided.  Unlike the List API as described above, the Search API’s positions are linear and sequential.

With these two pieces of information, we can implement Previous, Next and calculate the position of each page.  To fetch the page, we add it to the query string and set the start row like this:

page = manageQueryStringParameter(pageURLParameter);
if (page != “”) {
     keywordQuery.set_startRow(page * rowsPerPage);
}

The start row will be the position in the search results based on the current page and the number of results you’re displaying per page.  Since startRow starts at zero, page 1 should be page 0, page 2 should be page 1, etc. 

Calculating previous, next and each page is now straightforward by simply setting the page in the query parameter.  Since we know the total number of rows and the current page, we can enable/disable the previous and next links and create links for each page.

// calculate paging                    
var totalRows = results.m_value.ResultTables[0].TotalRows;
if (page == “”)
     page = 0;
else
     page = parseInt(page);
                    
if (page > 0)
{
       var prevPage = page -1;
     $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + prevPage + “‘>Prev</a>”);
}                   


var totalPages = totalRows / rowsPerPage;
for (i = 0; i<totalPages; i++)
{
       // count with a 0 but display as a 1
       var pageDisplay = i + 1;
       $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + i + “‘>” + pageDisplay  + “</a> | “);
}   


if ((page + 1) * rowsPerPage < totalRows)
{
     var nextPage = page + 1;                       
     $(‘#pages’).append(“<a href=’./news-list?category=” + category + “&page=” + nextPage + “‘>Next</a>”);
}

One important note on the total rows property – it can be an estimated total.  The SharePoint Search API provides estimates if the search results are large (e.g. you have thousands of items coming back from the search).  There is a property on the result table called IsTotalRowsExact which you can check to see if the total rows property value is exact or an estimate.

  • elie fares

    Very interesting! Would you have any thoughts about why choose an approach over the other, i.e performance-wise.
    Cheers,
    Elie

    • Christopher Woodill

      In general, I like the search approach better because it provides more control and allows you to aggregate across multiple document libraries or list. Performance wise, I find them both to be about the same but the list approach is less flexible. However, this also means that your content has to be indexed in the search to be available.

      • elie fares

        Okay! Thank you.

  • Алексей Алексеев

    what about ascending/descending?