Saturday, April 19, 2014

cheap infinite scroll in jquery

"Infinite scroll" is what you see on twitter and tumblr, where scrolling to the bottom of the current set of entries triggers the load of the next set.

I was looking for something similar for a work project. The jscroll project seemed to be it at first, but on closer inspection is about loading the next set of static content, and I was looking for a "simpler" trigger of a callback to load the next set of data via jQuery.

I ended up rolling my own, with a little help from the stack overflow entry Check if element is visible after scrolling.  (Note for this and the next code bit: for this project, my company uses the more verbose "jQuery()" rather than "$()" because of namespace conflicts. Also, I am encouraging the use of "jqo" as a Hungarian Notation prefix to indicate a jQuery Object.)

So when I set up the table, it's something like:
jqoTable = jQuery("<table class='mixgrowGrid'></table>");
jqoSpot.append(jqoTable);

jqoScrollTrigger = jQuery("<div class='mixgrowGrid_scrollTrigger'></div>");
jqoSpot.append(jqoScrollTrigger);

jQuery(window).scroll(checkForInfiniteScroll).resize(checkForInfiniteScroll);


I made a variable noMoreToLoad that is set to true when the query has returned all available records (in practice, this is set when the "next batch" query returns an empty array). So at the end of of my data receiving function, I write
if(!noMoreToLoad) checkForInfiniteScroll();

And then the supporting code is:

var checkForInfiniteScroll = function(){
    if(isTriggerScrolledIntoView()){
        if(!noMoreToLoad){
            loadNextSet();
        }
    }
}
var isTriggerScrolledIntoView = function(){
    var docViewTop = jQuery(window).scrollTop();
    var docViewBottom = docViewTop + jQuery(window).height();

    var elemTop = jQuery(jqoScrollTrigger).offset().top;
    var elemBottom = elemTop + jQuery(jqoScrollTrigger).height();

    return ((elemBottom >= docViewTop) && (elemTop <= docViewBottom)
        && (elemBottom <= docViewBottom) &&  (elemTop >= docViewTop) );
}

So why am I doing all this? "mixgrowGrid" is meant to be a substitute for jQuery datatables. Datatables is great if you want to take an existing html table on a page and bless it with client side functions like sorting and filtering. It handles infinite scrolling too, but poorly in my opinion: you pretty much have to have a fixed height display window (datatables seems more geared at pages where a table is just one element in the page flow, rather than being the center of attention, like it is on my company's dashboards) For the most part, I am less happy with widgets that carry their own scrollbar. My other beef with Datatables (besides its general complexity, which is par for the course for a genral purpose tool that tries to solve a LOT of different people's problems...) is that it is not great for mixed data; like if you have some parent/children rows, and especially if you would like some of the children rows to use "colspan". My mixgrowGrid says what it features in the name: mix-ed data (each type can have different column definitions) and designed to grow on the page.

There's a lot to do before this is ready for prime time: I will be making a footer that uses position:fixed to glue it to the bottom of the page, independent of the scrolling, and will probably have to do something similar for the headers, as well as install a "loading" notification. But one of the nice thing about rolling your own (the classic "Make vs Buy" conundrum) is that you can have a lot of control over that kind of detail, without having to learn and/or hack on someone else's big code base.

FOLLOWUP:
The header turned out to be pretty cool to do; I had to make a separate table above the main table (which means I had to make a separate function to make sure the widths were consistent with the main table, I didn't get that for free) and I had it be position:relative - the nice thing about that setting vs float is that it still preserves its space in the page flow, so things are simpler when it's "normally" visible, I don't have to think about making space for it. Then, on window scroll and resize (i.e. the same events as for infinite scrolling) I had this function, for the wrapper element (also position:relative, so that any children will be repositioned relative to it) and header table:
    var adjustHeaderLocation = function(){
        var eTop = jqoTableWrapper.offset().top; 
        var scrollTop = - jQuery(window).scrollTop();
        var newOffset =  -(scrollTop + eTop);
        if(newOffset < 0) newOffset = 0;
        jqoHeaderTable.css("top",newOffset+"px");
    }

FOLLOWUP TO THE FOLLOWUP:
I re-ran into a problem we had already solved, where it can be surprisingly tricky to make two tables the exact same width: border and padding and what not makes things weirder than you might expect. Some people suggest making a div version of the headers instead, but the short of it is you want to read the .outerWidth, not the .width()of the "td"s in question, and then apply that as the width, max-width, and min-width of the respective matching "th"s. Stackoverflow had some good discussion, including this bit of democode -- it looks like it could be a plugin, almost. I wasn't sure if the complex table I was building (with multirow headers) would use it as a drop-in, but I'll keep it in mind for the future.

1 comment: