Turn a Kentico repeater web part into an infinite scroll / lazy loader - IMEDIA Blog
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>
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


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.

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.
Rename the object display name, code name, file name, etc to what ever you feel is suitable.
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.
Click the properties tab to see the fields associated with the web part & add a new Category. I named my category Infinite Scroll Settings.
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.
- 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(); } });
- 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