The SharePoint JSOM Async Problem

Or how I came to love the executeQueryAsync method in sp.js

If you develop in SharePoint, whether an add-in, web part, or custom action, you will need to deal with the JavaScript Object Model (JSOM) provided by SharePoint. Of course, you could use the REST calls and get most of the same info, but for the one JSOM method to access all the properties, it can take at least three different REST calls, especially when dealing with library items. But the JSOM can get tricky, due to its async functionality. Let’s look at a simple call:

//Name your site
let siteUrl = '/sites/CoolPersonalSiteName';
function getListProps(){
    //get your SharePoint context
    let ctx = SP.ClientContext(site);
    //get your site
    let site = ctx.get_web();
    //and your specific list
    let lists = site.get_lists();
    //load your desired info
    ctx.load(lists);
    //execute the query to get the actual info of the lists
    ctx.executeQueryAsync((sender, args)=>{
        //setup your list loop
        let listEnum = lists.get_Enumerator();
        //move through each list item
        while(listEnum.moveNext()){
            let listItem = listEnum.get_current();
            //do what you want to each list item - here we print the title to the console
            console.log(listItem.get_title());
        }
    })
}

Here we are simply setting up what we want to load, in this case, the context of the specific list “AnotherCoolListName.” The Async isn’t bad as I am loading, looping, and printing information within the executeQueryAsync() function, so it comes out how I would expect.

But when do we deal with static values like “siteUrl” compared to dynamic ones? How often do we need SharePoint to tell us what a site name, list, etc is in order to do actions? What about multiple sites? Now things get complicated. Let’s do the same as the last example, but this time we want to load all the sites, go through their lists, and load all items in those lists. It may look like:

function getPropertiesFromAllListsOnAllSubsites(){
    let ctx = SP.ClientContext.get_current();
    //the getSubwebsForCurrentUser will ensure that you are getting all subsites, 
    //not just those associated to the user logged in by using null
    let allSites = ctx.get_web().getSubwebsForCurrentUser(null);
    ctx.load(allSites);
    ctx.executeQueryAsync((sender, args)=>{
        let siteEnum = allSites.getEnumerator();
        while(siteEnum.moveNext()){
            let site = siteEnum.get_current();
            let lists = site.get_lists();
            //This is a sp.js method that creates a recursive 
            //query to get all items within a list or library
            let qry = SP.CamlQuery.createAllItemsQuery();
            let items = lists.getItems(qry);

            ctx.load(lists);
            ctx.load(items);

            ctx.executeQueryAsync((sender, args)=>{
                let itemEnum = items.getEnumerator();
                while(itemEnum.moveNext()){
                    let item = listEnum.get_current();
                    //Do actions to list item properties and your app
                    //we are going to push these items to 
                    console.log(item);
                }
            })
        }
    }) 
}

This works, if you ran this on a SharePoint site and looked at the console, you would find a list of items from every site, meaning you could run logic, capture fields, or display items in your web part. Sure, it’s complicated, ugly, and there are async methods within async methods, but it gets the job done. But, again, it’s never that simple. What if you wanted to push everything to the DOM at once instead of when it gets to each item? Or push each item to an array to be used by a different method? What if you also want to store list info, now you are in an async loop in an async loop? What if you are stuck using IE, and nothing works because you can’t use the ‘let’ keyword and it stores variables a different way?

In any of these scenarios, you will find that not only does this get uglier, but it also starts to break.

Consider:

let siteArray = [];
ctx.load(allSites);
ctx.executeQueryAsync(()=>{
    //...
    while(siteenum.moveNext){
        let site = get_current();
        siteArray.push(site.get_title());
    }
});
console.log(siteArray)

If you were to run this code, the console would print an empty array. Since the execute method is async, it runs that method and moves to the next line, “console.log,” but since the “executeQuery” method hasn’t done anything yet, it hasn’t had a chance to push anything into the array. As you can imagine this would cause problems if you wanted to do logic to this section. And then there is the real kicker, what if you are stuck using IE (if this is the case, all I can say is, “I’m sorry, I’m so sorry”), then you can’t use the ‘let’ keyword. This means that another “executeQueryAsync” on each list to get to the list items, and then loop through those items, now imagine doing something with those items in another method.

Callbacks are your best friend, but when do you perform the callback? Remember, if you do an async method with a while loop the while loop will finish after you have already gotten to the callback. Not only that, but what if you have a site that doesn’t have a list on it, how do you deal with that scenario? Not to mention how ugly this starts to look when you get into the third layer of callback hell and are then trying to run methods within. It may seem like a lot of considerations, but I have created multiple web parts and have run into this quite frequently.

To get to all the info needed and use it in the best way possible, I use a combination of variables and strategically placed functions. (The following is written for IE10 and lower; it does not make use of JavaScript es6)

var ctx = SP.ClientContext.get_current();
var siteArray = [];
var listArray = [];
var itemArray = [];
var siteIndex = 0;
var listIndex = 0;

function loadSites(){
    var sites = ctx.get_web().getSubwebsForCurrentUser(null);
    ctx.load(sites);
    ctx.executeQueryAsync(function(sender, args) {
        var siteEnum = sites.getEnumerator();
        while(siteEnum.moveNext()){
            siteArray.push(siteEnum.get_current());
        }
        //After siteArray has been filled THEN call to get the lists from the sites
        loadLists();
    }, Function.createDelegate(this.failedQuery));
}

function loadLists(){
    siteArray.forEach(function(el){
        var lists = el.get_lists();
        ctx.load(lists);
        ctx.executeQueryAsync(function(sender, args){
            var listEnum = lists.getEnumerator();
            while(listEnum.moveNext()){
                listArray.push(listEnum.get_current());
            }
            //track the iteration through siteArray
            //you could use the index from the forEach
            //but I like to keep things similar and I must 
            //track index outside of foreach for failed attempts
            if(siteIndex === siteArray.length - 1){
                getItems();
            }
            siteIndex++;
        }, Function.createDelegate(this.failedQuery))
    })
}

function getItems(){
    var qry = SP.CamlQuery.createAllItemsQuery();
    listArray.forEach(function(el){
        var items = el.getItems(qry);
        ctx.load(items);
        ctx.executeQueryAsync(function(sender, args){
            var itemEnum = items.getEnumerator();
            while(itemEnum.moveNext()){
                var item = itemEnum.get_current();
                itemArray.push(item);
            }
            if(listIndex === listArray.length-1){
                //call the function to manipulate the site info, lists info, or items
                callYourNextFunction();
            }
            listIndex++;
        }, Function.createDelegate(this.failedItemQuery))
    })
};

function callYourNextFunction(){
    console.log('siteArray', siteArray);
    console.log('listArray', listArray);
    console.log('itemArray', itemArray);
}
//function to be called if a list does not exist on a site,
//this keeps the iteration going and calls the next function if
//the iteration is on the last run. Therefore I don't use callbacks
//this way I don't mess with the succeed and query function params already built in
function failedQuery(sender, args){
    if(siteIndex === siteArray.length-1){
        getItems();
    }
    siteIndex++;
};
function failedItemQuery(sender, args){
    if(listIndex === listArray.length-1){
        callYourNextFunction();
    }
    listIndex++;
}
//if run here would return empty arrays
callYourNextFunction();
//Now console returns filled arrays
loadSites();

This is a bit more complex, but you are also getting much more information. With this format, I can now run any logic on any of the information that is contained in either sites, lists, or items.

A few key points to review:

  • I track the indices of sites and lists outside of their forEach methods
    • I found that sometimes the query will fail because there is no list or item associated
    • In these times the index is still counted, and if on the last run, the callback is made
  • This will also work if a specific list is wanted
  • Code will work on any SharePoint site – add a web part that includes a code snippet and paste this in a script tag. Be sure to add the script tag for the sp.js file

And that is it. You now can take all sites, lists, and items and do as you please. Depending on your end function, you can display those items in any way you see fit.

Although this seems complex, it is one of the more straightforward items that we at Advisicon work with on a regular basis. This means that we can manipulate, organize, and change SharePoint to meet whatever needs you may have. So, let us know how we can help you, and we will work with you to get the solution you need. If you can imagine it, we can build it.

About

Leave a Comment

Advisicon is a Project, Program & Portfolio Management Company. We transform your organization's project management with a mix of methodology and technology that delivers results. Our team specializes in technology implementations, application and workflow development, training and consulting.
5411 NE 107th Ave, Suite 200
Vancouver
WA
98662
United States