Saturday, January 12, 2008

Implementing Endless Pageless Behaviour in wicket

Note:: I have edited the original code bit to support any element like div/table in addition to page.
Endless behavior of a page means you keep scrolling down and the page keeps loading the next view in the same page.
This has been used at many websites as a substitute for pagination and users are happy too.
An example is thoof .
I found the required javascript code at unspace website .

Today what I wanted to do was to implement such a behavior in wicket so that it can be added into any component, and customized.
There are only few things about the behavior you might want to change :

  1. how much distance before reaching the end of page you want to start an Ajax request.
  2. when are you out of further view? i.e. you don't want to load more view into your page once you are out of data.
Some Notes :
  1. The method of implementation is to check at regular intervals if you have reached towards the end of page scrolling.One should have a close look at
    hasMoreViewIncrementsIndicatorComponentId().In this method one should return the
    markup id of a hidden field. Also the model of the hidden field should be boolean. The hidden field repainted on every request to indicate if we have reached the end of the page or element.
  2. Also I would like to mention the use of WicketAjaxDebug in javascript. It will need you to turn on the wicket-ajax debug. Once turned on a link to ajax-debug window will be seen on page, and then it turns out to be really helpful in javascript debugging.


It turned out really very straightforward implementing the behavior. This behavior will send out Ajax request as soon as user is about to reach end of the page.

This is what the code finally looks like:




import org.apache.wicket.ajax.AbstractAjaxTimerBehavior;
import org.apache.wicket.ajax.AjaxRequestTarget;
import org.apache.wicket.markup.html.IHeaderResponse;
import org.apache.wicket.markup.html.resources.JavascriptResourceReference;
import org.apache.wicket.util.time.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class OnScrollEndUpdaterBahaviour extends AbstractAjaxTimerBehavior
{
private final int preLoadDistance;
private final Duration updateInterval;
protected static Logger log = LoggerFactory.getLogger(OnScrollEndUpdaterBahaviour.class);
private PageOrElement pgOrElement;
private String remainingScrollHeightFunction = "";

public enum PageOrElement {
PAGE, ELEMENT
};

public OnScrollEndUpdaterBahaviour(Duration updateInterval, int preLoadDistance,
PageOrElement pgOrElement)
{
super(updateInterval);
this.pgOrElement = pgOrElement;
this.preLoadDistance = preLoadDistance;
this.updateInterval = updateInterval;

}

@Override
protected void onBind()
{
switch (pgOrElement)
{
case PAGE :
remainingScrollHeightFunction = "getRemainingScrollHeight()";
break;
case ELEMENT :
remainingScrollHeightFunction = "getRemainingScrollHeight('"
+ getComponent().getMarkupId() + "')";
break;
}
super.onBind();
}

public OnScrollEndUpdaterBahaviour(int preLoadDistance, PageOrElement pgOrElement)
{
this(Duration.milliseconds(200), preLoadDistance, pgOrElement);
}

private static final JavascriptResourceReference PAGEUTILJS = new JavascriptResourceReference(
OnScrollEndUpdaterBahaviour.class, "pageutil.js");


@Override
public void renderHead(IHeaderResponse response)
{
super.renderHead(response);
response.renderJavascript("var preloadDistance = " + preLoadDistance + ";\n"
+ "var mouseState = 'up';\n" + "var hasMoreViewIncrements;"
+ "var canMakeAjaxCall= true;" + "\n" + "function onMouseDown(){\n"
+ "mouseState = 'down';\n" + "}\n" + "function onMouseUp(){\n"
+ "mouseState = 'up';\n" + "}\n" + "document.onmousedown = onMouseDown;\n"
+ "document.onmouseup = onMouseUp;\n" + "function pgTOCallBack(){"
+ getJsTimeoutCall(updateInterval) + "}", null);
response.renderJavascriptReference(PAGEUTILJS);
}

@Override
protected CharSequence getPreconditionScript()
{

return // can i make an ajax call?
"hasMoreViewIncrements=$('"
+ hasMoreViewIncrementsIndicatorComponentId()
+ "').value;"
+ "canMakeAjaxCall=( "
+ "mouseState == 'up' "
+ "&& ("
+ remainingScrollHeightFunction
+ " < preloadDistance ) "
+ "&& hasMoreViewIncrements=='true'"
+ ");"
+
// lets put some debug info
"WicketAjaxDebug.logInfo(' mouseState '+mouseState);"
+ "WicketAjaxDebug.logInfo(' getPageHeight() '+getPageHeight());"
+ "WicketAjaxDebug.logInfo('getScrollHeight()'+getScrollHeight());"
+ "WicketAjaxDebug.logInfo(' preloadDistance '+preloadDistance);"
+ "WicketAjaxDebug.logInfo(' hasMoreViewIncrements '+hasMoreViewIncrements);"
+
// if can make ajax call
"if(canMakeAjaxCall)" + "{" + "Wicket.Focus.lastFocusId='"
+ requestInProgressIndicatorComponentId() + "';"
+ "wicketShow('"
+ requestInProgressIndicatorComponentId()
+ "');"
+
// else set up another timeout
"}else{" + "wicketHide('" + requestInProgressIndicatorComponentId() + "');"
+ "pgTOCallBack();" + "}"
+ "WicketAjaxDebug.logInfo(' canMakeAjaxCall '+canMakeAjaxCall);"
+ "return canMakeAjaxCall;";
// && {"+super.getPreconditionScript()+"}
}

protected abstract String hasMoreViewIncrementsIndicatorComponentId();

protected abstract String requestInProgressIndicatorComponentId();

// doing this for better readability
@Override
protected void onTimer(AjaxRequestTarget target)
{
onPageEnd(target);
};

protected abstract void onPageEnd(AjaxRequestTarget target);

}


================================================================================
How to use the behavior:
component.add(new OnScrollEndUpdaterBahaviour(Duration.milliseconds(200), 50,
PageOrElement.ELEMENT)
{
@Override
protected void onPageEnd(AjaxRequestTarget target)
{
//also refresh the component you need to refresh
target.addComponent(hasMoreField);
}

@Override
protected String hasMoreViewIncrementsIndicatorComponentId()
{
return hasMoreField.getMarkupId();
}

@Override
protected String requestInProgressIndicatorComponentId()
{
return indicator.getMarkupId();
}
});
indicator.setOutputMarkupId(true);
================================================================================
pageutil.js
===========

function getPageHeight(){
var y;
var test1 = document.body.scrollHeight;
var test2 = document.body.offsetHeight
if (test1 > test2) {
y = document.body.scrollHeight;
} else {
y = document.body.offsetHeight;
}
return parseInt(y);
}

function _getWindowHeight(){
if (self.innerHeight) {
frameHeight = self.innerHeight;
} else if (document.documentElement && document.documentElement.clientHeight) {
frameHeight = document.documentElement.clientHeight;
} else if (document.body) {
frameHeight = document.body.clientHeight;
}
return parseInt(frameHeight);
}

function getScrollHeight(){
var y;
// all except Explorer
if (self.pageYOffset) {
y = self.pageYOffset;
} else if (document.documentElement && document.documentElement.scrollTop) {
y = document.documentElement.scrollTop;
} else if (document.body) {
y = document.body.scrollTop;
}
WicketAjaxDebug.logInfo('YYYYY: '+parseInt(y));
return parseInt(y)+_getWindowHeight();
}



function getElementHeight(elid){
var y;
var el= document.getElementById(elid);
var test1 = el.scrollHeight;
var test2 = el.offsetHeight
if (test1 > test2) {
y = el.scrollHeight;
} else {
y = el.offsetHeight;
}
return parseInt(y);
}

function _getElementWindowHeight(elid){
var el= document.getElementById(elid);
if (el.innerHeight) {
frameHeight = el.innerHeight;
} else if (el.clientHeight) {
frameHeight = el.clientHeight;
}
return parseInt(frameHeight);
}

function getElementScrollHeight(elid){
var y;
var el= document.getElementById(elid);
y = el.scrollTop;
WicketAjaxDebug.logInfo('YYYYY: '+parseInt(y));
return parseInt(y)+_getElementWindowHeight(elid);
}

function getRemainingScrollHeight(){
return getPageHeight() - getScrollHeight();
}

function getRemainingScrollHeight(elid){
return getElementHeight(elid) - getElementScrollHeight(elid);
}

2 comments:

Alex said...

Why not just assign an AjaxEventBehavior to window "onscroll", instead of checking at regular intervals if you have reached towards the end of page?

Also, my question is:
when the scroller has reached the bottom of the page, the entire component is reloaded via ajax or just the new data holder?

Thank you!

DontEatTooMuch said...

Alex,
sorry couldn't comment earlier..
if i assign the behaviour to "onscroll" i fear it will make the ui hang some bit if user keeps scrolling.

Also this is a behavior..it allows you to attach it to any component and then repaint it when you reach end of the page.

As soon as i get some time I am going to try write some code to create an endless list component whose increments can be painted using this behavior. Actually this behavior and such a list view should go hand in hand to create endless views in wicket.