Thursday, May 29, 2014

How to implement Infinite / Endless Scroll on your Visualforce Page

Introduction


Infinite scroll is a web technique which automatically and seamlessly loads more data as the user scrolls the page until all the data has been loaded. Anyone who uses Facebook knows what I am talking about. Other Web applications have implemented some sort of a semi infinite scroll, where a portion of the data is loaded as you scroll and after a while a "Load More" button is displayed. Checkout the comment section on Youtube to see this. Using the power of Visualforce & Ajax (with jQuery & JSON) and with Apex as your Backbone, you can build Force.com pages that implement these techniques easily. I will show you how. But first let's examine why you would want to use infinite scroll at all.

So what are the advantages of using infinite scroll and why is being adopted by many web applications?


  • it enhances User Experience which in my opinion is the most important factor when building web applications. Your users do not have to page back and forth through pages to find what they need. I know people who will argue that, letting users page gives them more control over the way they consume their content which makes them feel in control. I partially agree. As with every other feature or functionality on your page, you will have to give it some considerable thought before implementing it. Is continuous scroll going to help with usability? if yes then implement it. Also build in other features to help users jump to specific parts of the page. You do not want your users having to scroll all the way back to the top after they have scroll tons of data right to the end. This will have the opposite effect to what you are trying to achieve. So the bottom line is, if continuous scroll can add usability to your application, then use it. My motto is DON'T DO IT JUST BECAUSE YOU CAN.
  • the second and most important reason is the fact that, you relieve your system from having to handle large sets of data all at once. This is also directly tired to the first advantage. If you have been using Facebook heavily for the past 5 years, imagine how much time, it will take for Facebook to load all of your timeline data at once; this would include Text, images, comments, likes, etc. and all the necessary calculations required to link all the stuff you shared, stuff shared with you, links etc ... Despite the high speed Internet we have these days, it will take forever. So if you have large sets of data or require extensive work in the background while loading this data, and you do not want your users having to manually page through data, then endless or infinite scroll is you best option. 


With Infinite Scroll your will be able to load only portions of the entire data set at a time and load more as the user scrolls. This relieves your system from having to do a lot of heavy data retrieval at once which will most definitely be very resource exhaustive and at the same time prevents your users from long waiting periods while their data is prepared and loaded. It is seamlessly loaded as they scroll. Nice!

Implementation

I will not attempt to reinvent the wheel but will used a JavaScript / jQuery Library called Endless Scroll for my demonstration. I choose this because it is open source and quite easy to use.

Use Case

Build a custom account Page which displays account information 10 at a time. As the user scrolls, load more accounts, always 10 account records at a time.

Apex: Back-end

I will first of all show you how to prepare you data. In my opinion the front-end discussion makes more sense when you know what the back-end is doing

Controller - AccountBrowserController.cls


In the controller we will use the StandardSetController to retrieve the account information from the database. The StandardSetController can either take a list of sObjects or a query locator. With both methods you can retrieve up to 10000 records. The only difference is that with the query locator, you will receive a beautiful LimitException if you attempt to retrieve more than 10000 records whereas, with the sObject list, no exception is thrown. The list of records is just truncated.

For this demonstration, I used the query locator like this;
1:  public static final Integer PAGE_SIZE = 10;  
2:  public static Apexpages.Standardsetcontroller setCon {  
3:       get {  
4:            if(setCon == null){  
5:                 String query = 'SELECT Id, Name, AccountNumber, Site, OwnerId, ParentId, Ownership, Industry, AnnualRevenue, Type, Rating, Phone, Fax, Website, TickerSymbol, NumberOfEmployees, Sic FROM Account ORDER BY Name ASC';  
6:                 setCon = new Apexpages.Standardsetcontroller(Database.getQueryLocator(query));  
7:            }  
8:            return setCon;  
9:       }  
10:       set;  
11:  }  
With the StandardSetController, you can define how many records should be loaded with each subsequent call setPageSize(Integer). See the constant defined in line 1 above. You will probably want to set your limit a little higher. I am using 10 here because I do not have many accounts in my Dev Org.
The StandardSetController knows how many records your query will return in total and depending on the page size you set it has functions you can call to check if there is still more data to be loaded getHasNext(). You can set the page number setpageNumber(Integer) or get the number of the current page getPageNumber(). You can also request the records for the current page getRecords() or for the next page by using next(). These are the functions we require for our implementation as shown below;

1:  public static AccountWrapper buildScrollData(Integer currentPageNumber, Boolean getMore){  
2:       if(setCon != null){  
3:            setCon.setPageSize(PAGE_SIZE);  
4:            setCon.setPageNumber(currentPageNumber);  
5:            if(getMore){  
6:                 if(setCon.getHasNext()){  
7:                      setCon.next();  
8:                      List<Account> accts = (List<Account>)setCon.getRecords();  
9:                      if(accts != null && !accts.isEmpty()){  
10:                           AccountWrapper aw = new AccountWrapper(setCon.getPageNumber(), setCon.getHasNext(), true, accts);  
11:                           return aw;  
12:                      } else {  
13:                           AccountWrapper aw = new AccountWrapper(setCon.getPageNumber(), setCon.getHasNext(), false, null);  
14:                           return aw;  
15:                      }  
16:                 } else {  
17:                      AccountWrapper aw = new AccountWrapper(setCon.getPageNumber(), setCon.getHasNext(), false, null);  
18:                      return aw;  
19:                 }  
20:            } else {  
21:                 //Initial Call. Load the first number or records according to pageSize  
22:                 AccountWrapper aw = new AccountWrapper(setCon.getPageNumber(), setCon.getHasNext(), true, (List<Account>)setCon.getRecords());  
23:                 return aw;  
24:            }  
25:       }  
26:       return null;  
27:  }  

The first page returned is always 1. So initially we will set the current page to 1 and getMore to false and then use the getRecords() to retrieve the first 10 records (Line 22). In subsequent calls, if there are still more pages, we will use next() to set out counter up and getRecords() to retrieve the next 10 records. Note that we are wrapping these records using AccountWrapper Class before returning them to the page. This enables us to add more controls (hasMore, noRecords) to our page as shown below;

1:  public class AccountWrapper{  
2:       public Integer pageNumber { get; set; }  
3:       public Boolean hasMore { get; set; }  
4:       public Boolean noRecords { get; set; }  
5:       public List<Account> records { get; set; }  
6:       public AccountWrapper(Integer currentPageNumber, Boolean more, Boolean noRecs, List<Account> recordsForSinglePage){  
7:            pageNumber = currentPageNumber;  
8:            hasMore = more;  
9:            noRecords = noRecs;  
10:            records = recordsForSinglePage;  
11:       }  
12:  }  

If noRecords is true, the page doesn't make a call to the server.

Initially when the page is loaded, we retrieve the first set of records using the code below;

1:  public static String accountList {  
2:       get {  
3:            AccountWrapper aw = buildScrollData(1, false);  
4:            accountList = JSON.serialize(aw);  
5:            return accountList;  
6:       }  
7:       private set;  
8:  }  

For easy processing on the page, we return the account list as a JSON (JavaScript Object Notation) String.

Subsequent calls to the server will use JavaScript to get more data. This is possible by using JavaSccript Remoting for Apex Controllers. This enables the page to call Controller methods directly from JavaScript which is not possible with standard Visualforce components like actionFunction, actionSupport etc.

The remote method definition in the Controller is accomplished by using the @RemoteAction annotation;

1:  @RemoteAction  
2:  public static AccountBrowserController.AccountWrapper getMore(String currentPageNumber, String hasMore){  
3:       Integer pageNumber = Integer.valueOf(currentPageNumber);  
4:       Boolean more = Boolean.valueOf(hasMore);  
5:       if(more){  
6:            AccountBrowserController.AccountWrapper aw = AccountBrowserController.buildScrollData(pageNumber, more);  
7:            return aw;  
8:       }  
9:       return null;  
10:  }  

Here we do not need to send back a JSON string, because our remote function automatically converts our AccountWrapper object to a JavaScript object - cool stuff. A collection would automatically be converted to a JavaScript array and primitive types are also converted to their JavaScript equivalents. Really nice stuff.

So this concludes our Controller. Now let's look at what you need to do on the page to your continuous scroll up and running.

Visualforce Page: front-end


The first thing you need to do, is make sure you have jQuery and Endless Scroll included in your page. Usually I prefer to download these and have them in the Static Resources. Others prefer to include this from CDN. Not me.

1:  <apex:includeScript value="{!URLFOR($Resource.tutorials, '/js/jquery-1.11.1.min.js')}" />  
2:  <apex:includeScript value="{!URLFOR($Resource.tutorials, '/js/jquery.endless-scroll.js')}" />  

When the page is initially loaded I set the global variables, that I will use to make the call to the JavaScript remote function on the controller. This is necessary, because the remote function on the controller does not have access to the controllers other data and we will need to manually let it know the page number for instance.

1:  $(document).ready(function(){  
2:       var data = {!accountList};  
3:       currentPageNumber = data.pageNumber;  
4:       hasNext = data.hasMore;  
5:       noRecords = data.noRecords;  
6:       if(data.noRecords != false){  
7:            buildAccountRow(data.records);  
8:       }  
9:  });  
10:  var currentPageNumber = 0;  
11:  var hasNext = false;  
12:  var noRecords = false;  

I the use a javaScript function to build my data elements and then insert them to a div. As more records are retrieve using JavaScript remoting on the controller, the same function will be called to build more DOM elements (in this case tables) and append them to the DOM elements that already exist there.

1:  function buildAccountRow(records){  
2:       $.each(records, function(){  
3:            var moreposts = '';   
4:            moreposts += '<table class="grid_2_table" style="width:90%; border-style:solid;">';  
5:            moreposts += '<tr width="50%">';  
6:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Owner : </span>' + this.Owner + '</div></td>';  
7:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Rating: </span>' + this.Rating + '</div></td>';  
8:            moreposts += '</tr>';  
9:            moreposts += '<tr width="50%">';  
10:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Account Name: </span>' + this.Name + '</div></td>';  
11:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Phone: </span>' + this.Phone + '</div></td>';  
12:            moreposts += '</tr>';  
13:            moreposts += '<tr width="50%">';  
14:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Parent Account: </span>' + this.AccountNumber + '</div></td>';  
15:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Fax: </span>' + this.Fax + '</div></td>';  
16:            moreposts += '</tr>';  
17:            moreposts += '<tr width="50%">';  
18:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Account Number: </span>' + this.AccountNumber + '</div></td>';  
19:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Website: </span>' + this.Website + '</div></td>';  
20:            moreposts += '</tr>';  
21:            moreposts += '<tr width="50%">';  
22:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Account Site: </span>' + this.Site + '</div></td>';  
23:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Ticker Symbol: </span>' + this.TickerSymbol + '</div></td>';  
24:            moreposts += '</tr>';  
25:            moreposts += '<tr width="50%">';  
26:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Type: </span>' + this.Type + '</div></td>';  
27:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Ownership: </span>' + this.Ownership + '</div></td>';  
28:            moreposts += '</tr>';  
29:            moreposts += '<tr width="50%">';  
30:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Industry: </span>' + this.Industry + '</div></td>';  
31:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Employees: </span>' + this.NumberOfEmployees + '</div></td>';  
32:            moreposts += '</tr>';  
33:            moreposts += '<tr width="50%">';  
34:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">Annual Revenue: </span>' + this.AnnualRevenue + '</div></td>';  
35:            moreposts += '<td><div class="padding_bottom_10"><span class="bold">SIC Code: </span>' + this.Sic + '</div></td>';  
36:            moreposts += '</tr>';  
37:            moreposts += '</table>';  
38:            moreposts += '<br />';  
39:            $('#records').append(moreposts);  
40:       });  
41:  }  
42:  <apex:pageBlock title="Account Browser">  
43:       <div id="account_container">  
44:            <apex:outputPanel id="wrapper">  
45:            <div id="records">  
46:            </div>  
47:            </apex:outputPanel>  
48:       </div>  
49:  </apex:pageBlock  

JavaScript remoting requires that the method be called from the page (invocation) and the page must provide a callback function to handle the response from the server.

1:  function getMore(){  
2:       if(noRecords === true){  
3:            AccountBrowserController.getMore(currentPageNumber, hasNext, function(result, event){  
4:                 currentPageNumber = result.pageNumber;  
5:                 hastNext = result.hasMore;  
6:                 noRecords = result.noRecords;  
7:                 if(result.noRecords != false){  
8:                      if(event.status == true){  
9:                           buildAccountRow(result.records);  
10:                      }  
11:                 }  
12:            });  
13:       } else {  
14:            hasNext = false;  
15:       }       
16:  }  

If there are more records, we then call the getMore remote function passing in the current page number and the Boolean variable indicating that there is more data. The call back returns the results and the status of our request. If the request was successful, we build our DOM elements and add them to our page.We also update our global variables.

Créme de la Créme


My french is not that good, but I think Créme de la Créme means something like "the best part" and that is where we are now; the best part and core this article.

This is how to set up your endless scroll;

1:  $(document).endlessScroll({  
2:       fireOnce: true,  
3:       fireDelay: 1000,  
4:       bottomPixels: 10,  
5:       content: function(p){  
6:            getMore();  
7:       },  
8:       ceaseFireOnEmpty: true  
9:  });  

Endless Scroll hast a number of options you can set. These options let you fine tune the behavior of the endless scroll. Please check out the documentation to know more about available options. Here I have set a few options;
  • fireOnce: fire only once until the excution of the current event is completed

  • fireDelay: delays subsequent firingby 1 second. Within this period, endless scroll is disabled.

  • content: content to insert after each call

  • ceaseFireOn Empty: endless scroll is disabled when the content returned is empty.


As mentioned above, there are a few more options you can set to control the behavior of your endless scroll implementation. Play around with this a little and understand how everything works. Then set the options that work for you.

Conclusion

As mentioned in the introduction, endless or infinite scroll is a web technology that is very useful, but nonetheless it should be used carefully and only after considerable thought. There are other JavaScript libraries that can help you implement infinite scroll easily. Do your homework and choose what you think will work for you best. The Back-end solution will probably not change much.
I do my best to give my readers an end-to-end solution, that I have tested and it works. You can then use this solution as the basis of your own implementation. I hope you were able to learn something from this article. feel free to challenge it and may be even offer better solutions which will benefit us all especially future readers. I thank you for taking the time to read.

No comments:

Post a Comment