Share This
 
All Posts  /  Kentico Development Tips  /  December 03, 2015 

Turn a Kentico repeater web part into an infinite scroll / lazy loader

Infinite scroll and lazy loading are popular ways to display data on web sites. Kentico repeaters provide developers with a way to easily get data and limit the results via the web part configuration and filters. However, if there is a lot of data, developers and editors are limited to using pagination. In this post I will walk you through turning a Kentico repeater web part into an SEO friendly infinite scroll / lazy loader that defaults to pagination if JS is not enabled.
 

If you are just here for the actual web part, you can download it here. Upload the zip into Kentico by going to Sites > Import site or objects. Make sure you check Import Code files. If you need to edit the code, you can find it in WebParts/iMediaInc/InfiniteScrollRepeater.ascx. When you are configuring the web part, ensure that pagination is enabled. If you don’t, the lazy load / infinite scroll won't work. Also ensure that you are using jQuery. 

<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
Import Site or Objects
Check Import Code Files
 

When configuring the web part you can select whether to use infinite scroll or lazy load. If both are selected by accident, it defaults to infinite scroll. If lazy load is selected, you have to also enter the class for the button/link designated for the click event. If you want to call a method to show a loading animation while loading the data, enter the name of the method in the LazyLoadingMethod field. This will work for both infinite scroll and lazy load. Check the Use URL update box if you want the URL to update in the browser as the user scrolls. Also enter the header height if your header is very tall and the page URL doesn't appear to be updating with the right number. Under the Paging section, ensure that Enable Paging is checked, and that the page size isn't too large

Infinite Scroll Settings Pagination settings

 

Make sure that all of the text in your transformation is wrapped in HTML tags, otherwise there will be a javascript error when the returned data is parsed.

Transformation

 


If you are here to learn how to make your own infinite scroll / lazy load repeater, read on!
 

First start by cloning Kenticos Repeater web part. Go to Web Parts > Search for Repeater > click the dots next to the repeater & select Clone. 
Clone Web Part


Rename the object display name, code name, file name, etc to what ever you feel is suitable.
clone web part configuration

 

After the web part has been created, you will need to add some additional fields. Navigate to your new web part in the CMS & click the green pencil to edit it.
edit webpart

 

Click the properties tab to see the fields associated with the web part & add a new Category. I named my category Infinite Scroll Settings.
Web part properties

 

You will need to add the following fields to the web part under the new category.

  • Field Name: UseLazyLoad
    Data Type: Boolean
    Default Value: No
    Field Caption: Use Lazy Load
    Form Control: Check box
    This field allows the admin to choose if they want to use the lazy load function or the infinite scroll.
     
  • Field Name: LazyButtonClass
    Data Type: Text
    Field Caption: Lazy Load Button Class
    Form Control: Text Box
    This field is required if the lazy load feature is being used. It tells the program which click event to listen for when more items need to be loaded.
     
  • Field Name: LazyLoadingMethod
    Data Type: Text
    Field Caption: Lazy Loading Method
    Form Control: Text Box
    This field allows the developer to call a specific javascript function when ajax has requested, and is waiting for more more data from the server. This is where the developer might display an animated gif to let the user know that more data is loading. It can be used with both lazy load and infinite scroll.
     
  • Field Name: UseInfiniteScroll
    Default Value: Yes
    Data Type: Boolean
    Field Caption: Use Infinite Scroll
    Form Control: Check Box
    This field allows the admin to choose if they want to use the lazy load function or the infinite scroll.
     
  • Field Name: UseURLUpdate
    Default Value: Yes
    Data Type: Boolean
    Field Caption: Use URL Update
    Form Control: Check Box
    Enable if you want the URL to update as new pages are loaded.
     
  • Field Name: HeaderHeight
    Data Type: Integer Number
    Field Caption: Header Height
    Form Control: Text Box
    The height of the content that appears before the repeater content. If not properly set, the URL may not update on the correct page.
 

Next, add the associated properties into the ascx.cs file.

#region infinite scroll properties

     /// <summary>
    /// Gets or sets the value that indicates whether to use the lazy load.
    /// </summary>

    public bool UseLazyLoad
    {
        get
        {
            return ValidationHelper.GetBoolean(GetValue("UseLazyLoad"), false);
        }
        set
        {
            SetValue("UseLazyLoad", value);
        }
    }
    /// <summary>
    /// Gets or sets the name of the class used to load more pages for the lazy loader.
    /// </summary>
    public string LazyButtonClass
    {
        get
        {
            return DataHelper.GetNotEmpty(GetValue("LazyButtonClass"), "");
        }
        set
        {
            SetValue("LazyButtonClass", value);
        }
    }
    /// <summary>
    /// Gets or sets the name of the method to call when the loading starts and ends.
    /// </summary>
    public string LazyLoadingMethod
    {
        get
        {
            return DataHelper.GetNotEmpty(GetValue("LazyLoadingMethod"), "");
        }
        set
        {
            SetValue("LazyLoadingMethod", value);
        }
    }
    /// <summary>
    /// Gets or sets the value that indicates whether to use the infinite scroll.
    /// </summary>
    /// 
    public bool UseInfiniteScroll
    {
        get
        {
            return ValidationHelper.GetBoolean(GetValue("UseInfiniteScroll"), false);
        }
        set
        {
            SetValue("UseInfiniteScroll", value);
        }
    }


    /// <summary>
    /// Gets or sets the value that indicates whether to use the url update.
    /// </summary>
    public bool UseURLUpdate
    {
        get
        {
            return ValidationHelper.GetBoolean(GetValue("UseURLUpdate"), false);
        }
        set
        {
            SetValue("UseURLUpdate", value);
        }
    }

    /// <summary>
    /// Gets or sets the value of the height of the page header.
    /// </summary>
    public int HeaderHeight
    {
        get
        {
            return ValidationHelper.GetInteger(GetValue("HeaderHeight"), 1);
        }
        set
        {
            SetValue("HeaderHeight", value);
        }
    }
    #endregion
 

The number of pages and other info needs to be passed to the browser. To do this, add an event handler to the OnContentLoaded method to call a method after the repeater items have loaded.


    /// <summary>
    /// Content loaded event handler.
    /// </summary>
    public override void OnContentLoaded()
    {
        base.OnContentLoaded();
        SetupControl();
        repItems.Load += new EventHandler(this.ReturnScript);
    }
 

In your method, collect all of the properties & info you want to send from the back end & pass it to a label.


    protected void ReturnScript(object sender, EventArgs e)
    {
        string js = "<script>";
        //Pagination has to be enabled for this to work properly
        if (EnablePaging)
        {
            js += "total_pages=" + repItems.PagerControl.PageCount.ToString() + ";";
            js += "param_name='" + QueryStringKey + "';";
            js += "use_url_update ='" + UseURLUpdate.ToString() + "';";
            js += "$('.PagerControl').remove();";
            js += "loader_method = '" + LazyLoadingMethod + "';";
            if (UseInfiniteScroll)
            {   
                js += "header_height=" + HeaderHeight + ";";               
                js += "initPaginator();";
                js += "</script>";
                lblJS.Text += js;
            }
            if (UseLazyLoad && !UseInfiniteScroll)
            {                
                js += "lazy_button_class='" + LazyButtonClass + "';";
                js += "InitLazyLoad();";
                js += "</script>";
                lblJS.Text += js;
            }
        }
    }
 

In the .ascx file add the following divs around the CMSRepeater tag.


<div id="scrollingcontent">
    <div class="scrollingcontentblock" data-page="1">
        <cms:CMSRepeater ID="repItems" runat="server" />
    </div>
</div>
 

Add the label to the bottom of the page.

<asp:Label ID="lblJS" runat="server"></asp:Label>
 

Now comes the fun part, the javascript! Much of the script I used was taken from John Muller's SEO friendly infinite scroll demo. I will walk you through the infinite scroll first, then the lazy load, providing brief snippits of code to give you a general idea of how it works. The entire script will be fully compiled at the bottom. 
 

Infinite Scroll

If the infinite scroller is enabled in the web part configuration, the InitPaginator function is called when the repeater items have initially loaded (remember repItems.Load += new EventHandler(this.ReturnScript);?). InitPaginator has two major purposes.

  1. After the page has loaded the function gets the page parameter from the url and requests that the data for the previous and next pages, if available, are cached. This allows the previous/next items to load faster.
    $(document).ready(function () {
         url = window.location.href;
         var pg_param = getParameterByName(param_name);
         //Check to see if there is a page parameter
         if (pg_param != "") {
             currentpg = pg_param;
             $('.scrollingcontentblock').attr("data-page", currentpg);
             if (currentpg > 1) {
                  prev_data_page = parseInt(currentpg) - 1;
                  prev_data_url = replaceUrlParam(url, param_name, prev_data_page);
                  loadPrevious();
              }
          }
          next_data_page = parseInt(currentpg) + 1;
          next_data_url = replaceUrlParam(url, param_name, next_data_page);
          // if we have enough room, and there are more pages to load, load the next batch
          if (next_data_page <= total_pages) {
               loadFollowing();
           }
    }); 
  2. When the page is scrolled the scroll position is calculated to determine if it is time to load the items from the previous or next pages. Since the immediate previous/next items are always stored for quicker loading, you are actually loading the page following the immediate request. A function is also called to update the URL with the correct page number.
    $(window).scroll(function () {
        // handle scroll events to update content
        var scroll_pos = $(window).scrollTop();
        if (scroll_pos >= 0.5 * ($(document).height() - $(window).height())) {
            if (is_loading == 0 && (currentpg < total_pages)) {
                loadFollowing();
                currentpg++;
              }
         }
         if (scroll_pos <= header_height) {
             if (is_loading == 0 && prev_data_page >= 1) {
                  //ensure the page doesnt already exist
                   var elements = $('#scrollingcontent').find('.scrollingcontentblock[data-page=' + prev_data_page + ']');
                   if (elements.length < 1) {
                       loadPrevious();
                   }
               }
           }
           if (use_url_update == "True") {
                replaceURL(scroll_pos, last_scroll);
           }
      });
 

When loadPrevious or loadFollowing are called, the ajax shorthand $.get is used to request the previous or next page from the server. If a loader method is being used, it is called once just before the GET request and again after ajax is done getting the data. This allows the developer to show a div while loading and hide it when loading is complete.

 if (loader_method != '') {
    try{
        window[loader_method]();
    catch(err){}
  }
  $.get(next_data_url, function (data) {
      showFollowing(parseHTML(data,next_data_page));
      is_loading = 0;
      //Call loading
  }).done(function () {
     //End loading
     if (loader_method != '') {
         try{
             window[loader_method]();
         } catch(err){}
     }
  });
 

Ajax is requesting the whole page you are on with the next set of data items, so the new items need to be parsed from that page before they are appended to the other items on the page. parseHTML handles this, hides the pagination, and adds the page number to that pages content block so that we can easily grab it and update the URL while the user scrolls.

//Since the whole page is returned - we just need the new page data
 function parseHTML(data, pg) {
    var contents = $(data).find("#scrollingcontent").html();
    //remove the pagination
    contents = $($(contents).html()).not(".PagerControl");
    contents = $('<div/>').append($(contents).clone()).html();
    contents = "<div class='scrollingcontentblock'>" + contents + "</div>";
    //update the page num
    contents = $(contents + ".scrollingcontentblock").attr("data-page", pg);
    return contents;
  }


Lazy Load

If the lazy load is enabled in the web part configuration, and the infinite scroll is disabled, InitLazyLoad is called when the repeater items have initially loaded. While the infinite scroll can load both previous and next pages, the lazy load only loads the next page in cache. If desired, functionality could be built in to load previous pages on a button click if the initial request for a page was greater than page 1, but that feature is not included here. InitLazyLoad determines the next page number and loads that page in cache, it also controls updating the scroll position and updating the page parameter in the browser.
 

The lazy loader works a lot like the infinite scroll, however the ajax $.get request does not happen until the lazy button class is clicked. Once there are no more pages to load, the load more button is hidden.


$("." + lazy_button_class).click(function () {
   if(loading == 0){
   //start the loader
    if (loader_method != '') {  
         try{
             window[loader_method]();
         } catch(err){}
     }     //append the cached next page
     $('div.scrollingcontentblock:last').after($(next_data_cache).hide().fadeIn());
         currentpg++;
         //then load the next cache
         next_data_page = parseInt(currentpg) + 1;
         if (next_data_page < total_pages) {                
              //Load next page into cache
              next_data_url = replaceUrlParam(url, param_name, next_data_page);
              loading = 1;                    
              $.get(next_data_url, function (data) {                                  
                   //parse & append the data
                   next_data_cache = parseHTML(data, next_data_page);    
              }).done(function () {
                   //End loading
                   if (loader_method != '') {
                         try{
                             window[loader_method]();
                         } catch(err){}
                   }
                   loading = 0;
               });                
           }
           else {
              $("." + lazy_button_class).hide();
           }
        }
    });
});        

 

The whole script:


    
    var loader_method = ''; //method to call while waiting for new pages to load    
    var last_scroll = 0;
    var use_url_update = "True";    //Update the url as the user scrolls
    var total_pages = 0;  //provided from the back end PagerControl.PageCount
    var currentpg = 1;
    var url = '';  //url of the page including any params
    var param_name = ""; //provided from the web part Query String Key    
    var header_height = 10; //Height of the page header
    var next_data_page = 1; // replaced when loading more
    var prev_data_page = 1; // replaced when loading more
    var next_data_url = replaceUrlParam(url, param_name, next_data_page); // replaced when loading more
    var prev_data_url = replaceUrlParam(url, param_name, prev_data_page); // replaced when loading more   
    var next_data_cache;
    var prev_data_cache;
    var is_loading = 0; // simple lock to prevent loading when loading  
    
    /////////////////////////////////////////////////////
    /////////////INFINITE SCROLL ///////////////////////
    ///////////////////////////////////////////////////
    
     
    function loadFollowing() {
        if (next_data_page > total_pages) {
        } else {
            is_loading = 1; // note: this will break when the server doesn't respond
            function showFollowing(data) {
                $('div.scrollingcontentblock:last').after(data);
                if (next_data_page <= total_pages) {
                    next_data_page++;
                }
                next_data_url = replaceUrlParam(url, param_name, next_data_page);
                next_data_cache = false;
                $.get(next_data_url, function (preview_data) {
                    next_data_cache = parseHTML(preview_data, next_data_page);
                    //Call loading                   
                }).done(function () {
                    //End loading                    
                });
            }
            if (next_data_cache) {
                showFollowing(next_data_cache);
                is_loading = 0;
            } else {
                if (loader_method != '') {
                        try{
                            window[loader_method]();
                        } catch(err){}
                    }
                $.get(next_data_url, function (data) {
                    showFollowing(parseHTML(data,next_data_page));
                    is_loading = 0;
                    //Call loading
                }).done(function () {
                    //End loading
                    if (loader_method != '') {
                        try{
                        window[loader_method]();
                        } catch(err){}
                    }
                });
            }
        }
    }

    function loadPrevious() {
        if (prev_data_page == 0) {
        } else {
            is_loading = 1; // note: this will break when the server doesn't respond
            function showPrevious(data) {
                $('div.scrollingcontentblock:first').before(data);
                item_height = $("div.scrollingcontentblock:first").height();
                window.scrollTo(0, $(window).scrollTop() + item_height + header_height); // adjust scroll                            
                prev_data_page--;
                prev_data_url = replaceUrlParam(url, param_name, prev_data_page);
                prev_data_cache = false;
                $.get(prev_data_url, function (preview_data) {
                    prev_data_cache = parseHTML(preview_data, prev_data_page);
                    //Call loading
                }).done(function () {
                 //End loading
                });
            }
            if (prev_data_cache) {
                showPrevious(prev_data_cache);
                is_loading = 0;
            } else {
                if(loader_method != ''){
                    try{
                        window[loader_methos]();
                    } catch(err){}
                }
                $.get(prev_data_url, function (data) {
                    showPrevious(parseHTML(data, prev_data_page));
                    is_loading = 0;
                    //Call loading                    
                }).done(function () {
                    //End loading 
                    if (loader_method != '') {
                        try{
                            window[loader_method]();
                        } catch(err){}
                    }
                });
            }
        }
    };
    
    function initPaginator() {
        $(window).scroll(function () {
            // handle scroll events to update content
            var scroll_pos = $(window).scrollTop();
            if (scroll_pos >= 0.5 * ($(document).height() - $(window).height())) {
                if (is_loading == 0 && (currentpg < total_pages)) {
                    loadFollowing();
                    currentpg++;
                }
            }
            if (scroll_pos <= header_height) {
                if (is_loading == 0 && prev_data_page >= 1) {
                    //ensure the page doesnt already exist
                    var elements = $('#scrollingcontent').find('.scrollingcontentblock[data-page=' + prev_data_page + ']');
                    if (elements.length < 1) {
                        loadPrevious();
                    }
                }
            }
            if (use_url_update == "True") {
                replaceURL(scroll_pos, last_scroll);
            }
        });
        $(document).ready(function () {
            url = window.location.href;
            var pg_param = getParameterByName(param_name);
            //Check to see if there is a page parameter
            if (pg_param != "") {
                currentpg = pg_param;
                $('.scrollingcontentblock').attr("data-page", currentpg);
                if (currentpg > 1) {
                    prev_data_page = parseInt(currentpg) - 1;
                    prev_data_url = replaceUrlParam(url, param_name, prev_data_page);
                    loadPrevious();
                }
            }
            next_data_page = parseInt(currentpg) + 1;
            next_data_url = replaceUrlParam(url, param_name, next_data_page);
            // if we have enough room, and there are more pages to load, load the next batch            
            if (next_data_page <= total_pages) {
                loadFollowing();
            }            
        });
    }

    
  

    ///////////////////////////////////////////////////
    /////////////////////LAZY LOAD////////////////////
    /////////////////////////////////////////////////
    //Lazy Load Settings
    var lazy_button_class = ''; //button class for the lazy loader to load more pages
    
    //Handle Lazy Load Option
    function InitLazyLoad() {
        $(window).scroll(function(){
            var scroll_pos = $(window).scrollTop();
            if (use_url_update == "True") {
                replaceURL(scroll_pos, last_scroll);
            }
        });
    
        $(document).ready(function () {
            url = window.location.href;
            //if the pages load with a page param we can only load the 
            //following pages, not previous pages
            var pg_param = getParameterByName(param_name);
            if (pg_param != '') {
                currentpg = pg_param;
                $('.scrollingcontentblock').attr("data-page", currentpg);                
            }
            next_data_page = parseInt(currentpg) + 1;
            if (next_data_page < total_pages) {                
                //Load next page into cache
                next_data_url = replaceUrlParam(url, param_name, next_data_page);
                loading = 1;                
                $.get(next_data_url, function (data) {                                  
                    //parse & append the data
                    next_data_cache = parseHTML(data, next_data_page);
                }).done(function () {
                    //End loading
                    if (loader_method != '') {
                        try{
                            window[loader_method]();
                        } catch(err){}
                    }
                    loading = 0;
                });                
            } else {
                $("." + lazy_button_class).hide();
            }    
            
            //lazy button click to load more
            $("." + lazy_button_class).click(function () {
                if(loading == 0){
                    //start the loader
                    if (loader_method != '') {
                        try{
                            window[loader_method]();
                        } catch(err){}
                    }
                    //append the cached next page
                    $('div.scrollingcontentblock:last').after($(next_data_cache).hide().fadeIn());
                    currentpg++;
                    //then load the next cache
                    next_data_page = parseInt(currentpg) + 1;
                    if (next_data_page < total_pages) {                
                        //Load next page into cache
                        next_data_url = replaceUrlParam(url, param_name, next_data_page);
                        loading = 1;                    
                        $.get(next_data_url, function (data) {                                  
                            //parse & append the data
                            next_data_cache = parseHTML(data, next_data_page);    
                        }).done(function () {
                            //End loading
                            if (loader_method != '') {
                                try{
                                    window[loader_method]();
                                } catch(err){}
                            }
                            loading = 0;
                        });                
                    }
                    else {
                        $("." + lazy_button_class).hide();
                    }
                }
            });
        });        
    }

        
    
    ////////////////////////////////////////////////////////
    //Global functions
    //////////////////////////////////////////////////////

    //Since the whole page is returned - we just need the new page data
    function parseHTML(data, pg) {
        var contents = $(data).find("#scrollingcontent").html();
        //remove the pagination
        contents = $($(contents).html()).not(".PagerControl");
        contents = $('<div/>').append($(contents).clone()).html();
        contents = "<div class='scrollingcontentblock'>" + contents + "</div>";
        //update the page num
        contents = $(contents + ".scrollingcontentblock").attr("data-page", pg);
        return contents;
    }

    function replaceURL(scroll_pos, last_scroll){
    // Adjust the URL based on the top item shown
        if (Math.abs(scroll_pos - last_scroll) > $(window).height() * 0.1) {
            last_scroll = scroll_pos;
            $('.scrollingcontentblock').each(function (index) {
                if (mostlyVisible(this)) {
                    var page_replace = replaceUrlParam(url, param_name, $(this).attr("data-page"));
                    history.replaceState(null, null, page_replace);                        
                    //Add GA code here for page clicks
                    return (false);
                }
            });
        }
    }
    
    function mostlyVisible(element) {
        // if ca 25% of element is visible
        var scroll_pos = $(window).scrollTop();
        var window_height = $(window).height();
        var el_top = $(element).offset().top;
        var el_height = $(element).height();
        var el_bottom = el_top + el_height;
        return ((el_bottom - el_height * 0.25 > scroll_pos) &&
          (el_top < (scroll_pos + 0.5 * window_height)));
    }
    
    //Handles the url in case there are any parameters
    function replaceUrlParam(url, paramName, paramValue) {
        var pattern = new RegExp('\\b(' + paramName + '=).*?(&|$)')
        if (url.search(pattern) >= 0) {
            return url.replace(pattern, '$1' + paramValue + '$2');
        }
        return url + (url.indexOf('?') > 0 ? '&' : '?') + paramName + '=' + paramValue
    }

    function getParameterByName(name) {
        name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
        var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
        results = regex.exec(location.search);
        return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
    }


 

The full code files:
InfiniteScrollRepeater.zip

Show More
Share This
 
Comments
Blog post currently doesn't have any comments.
*
 Security code
*
ADD COMMENT
Social Buzz

Get in Touch

Contact

Contact
t: 973.539.5255
f: 973.917.4730

Visit

Visit
715 Main Street
Boonton, NJ 07005

Locations

Locations
Boonton
Dallas
Jersey City
Boulder

Send Us a Message




 Security code