Steven Verborgh

Old Entries for those of you on a journey

Supporting jquery ClientBehavior in JSF 2.0

Feb 12, 2010

In JavaServer Faces 2.0 they have attempted to integrate Ajax and javascript. In an attempt to make it as easy as possible, and to increase the pluggability of the entire framework, the decision was made to come up with the concept of ClientBehavior. This seems to have been inspired by the presence of f:validator and f:convertor. Non Gui components that add extra functionality to their parent tags.

In theorie this is ok, but the events to which you can attache behaviour, are limited to the events returned by the getEventNames() method of ClientBehaviourHolder. This interface is implemented by some components. For some reason the Specification team decided that in the HTML renderkit only the editable value holder should implement this interface, and that the only events that are supported are the official dom events. The last part I can understand since the HTML renderkit does only try to specify the minimum requirements.

In an attempt at integrating f:ajax for custom jquery events I ran into the limitations this imposes in relation to the front end development. We all know ASP.NET uses jquery, and it is a toolkit that is impossible to overlook at the moment. In this small howto we will write a jquery uicomponent that allwos us to attach f:ajax behaviour to it so we can invoke remote methods from backingbeans. The end result will look like this:

<h:form id="form">
    <h:panelGroup layout="block" id="draggable">
        <div><h:outputText value="Drag me"/></div>
        <howest:jquery events="dragstart,drag,dragstop" default="dragstop" plugin="draggable" options="{handle: 'div'}">
            <f:ajax listener="#{sample.doStart}" render="status" event="dragstart"/>
            <f:ajax listener="#{sample.doDrag}" render="status" event="drag"/>
            <f:ajax listener="#{sample.doStop}" render="status" event="dragstop"/>
        </howest:jquery>
    </h:panelGroup>

    <h:outputText id="status" value="#{sample.status}" />
</h:form>

We will be using ResourceDepency to inject the correct jquery libraries. The tag as you can see will allow us to create composite tags that use jquery. For example a draggable tag or whatever you want.

You can get the code from github here. It's a slightly modified maven library and demo project that should get you up and running.

Lets look at the code we are going to need, so we can examine piece by piece afterward:

package be.howest.faces.components;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.faces.application.ResourceDependencies;
import javax.faces.application.ResourceDependency;
import javax.faces.component.FacesComponent;
import javax.faces.component.UIComponentBase;
import javax.faces.component.behavior.ClientBehavior;
import javax.faces.component.behavior.ClientBehaviorContext;
import javax.faces.component.behavior.ClientBehaviorHolder;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;

@FacesComponent("jquery")
@ResourceDependencies({
    @ResourceDependency(name = "jsf.js", target = "head", library="javax.faces"),
    @ResourceDependency(name = "jquery.js", target = "head"),
    @ResourceDependency(name = "jquery-ui.js", target = "head")
})
public class JQuery extends UIComponentBase implements ClientBehaviorHolder {

    private static final String FAMILY = "jquery";

    private static final String TEMPLATE_EVENT = "\n$('#%s').bind('%s', function(event, ui) { %s; });\n";
    private static final String TEMPLATE_PLUGIN = "\n$('#%s').%s(%s);\n";

    @Override
    public String getFamily() {
        return FAMILY;
    }

    @Override
    public void encodeBegin(FacesContext context) throws IOException {
        String clientId = getParent().getClientId();
        String jQueryId = clientId.replace(":", "\\\\:");

        ResponseWriter responseWriter = context.getResponseWriter();
        responseWriter.startElement("script", null);
	
        Map> behaviors = getClientBehaviors();
        for (String eventName : behaviors.keySet()) {
            if (getEventNames().contains(eventName)) {

                ClientBehaviorContext behaviorContext = ClientBehaviorContext
                        .createClientBehaviorContext(context, this, eventName, clientId, null);

                String jsfJsCode = behaviors.get(eventName)
                        .get(0).getScript(behaviorContext);

                String jsCode = String.format(TEMPLATE_EVENT, jQueryId, eventName, jsfJsCode);

                responseWriter.writeText(jsCode, null);
            }

        }
        String plugin = (String) getAttributes().get("plugin");
        if(null != plugin) {
            String options = (String) getAttributes().get("options");
            if(options == null) options = "";
            String jsCode = String.format(TEMPLATE_PLUGIN, jQueryId, plugin, options);
            responseWriter.writeText(jsCode, null);
        }

        responseWriter.endElement("script");
    }

    @Override
    public void decode(FacesContext context) {
        Map> behaviors = getClientBehaviors();
        if (behaviors.isEmpty()) {
            return;
        }

        ExternalContext external = context.getExternalContext();
        Map params = external.getRequestParameterMap();
        String behaviorEvent = params.get("javax.faces.behavior.event");
        if (behaviorEvent != null) {
            List behaviorsForEvent = behaviors.get(behaviorEvent);
            if (behaviors.size() > 0) {
                String behaviorSource = params.get("javax.faces.source");
                String clientId = getParent().getClientId(context);
                if (behaviorSource != null && behaviorSource.equals(clientId)) {
                    for (ClientBehavior behavior : behaviorsForEvent) {
                        behavior.decode(context, this);
                    }
                }
            }
        }
    }

    @Override
    public Collection getEventNames() {
        return Arrays.asList(((String) getAttributes().get("events")).split(","));
    }

    @Override
    public String getDefaultEventName() {
        return (String) getAttributes().get("default");
    }
}

The first line we are going to look at is this:

public class JQuery extends UIComponentBase implements ClientBehaviorHolder

As you can see, we are creating a new UIComponent. This has many bad side effects like showing up in the hierarchy tree and messing up panelGrids and that. But we can't create a behaviour or whatever since, as mentioned aboven, only "DOM"-events are supported for behaviours. Also, since this is a UIComponent that is going to handle JQuery events, and we want to attach ClientBehaviour to those JQuery events, we are going to have te implement ClientBehaviourHolder

On top of the class declaration you also see the @FacesComponent("jquery") annotation. Every UIComponent needed in ye olde days a registration in faces-config.xml. This associated a component type to a class. As usual this is now possible by adding an annotation. So here we define the type of the component.

Next to a type, each component needs a component family. (like Command, Output,...). Since this component is merely a hack, I defined my own family type: jquery. It is of no use for other renderkits (that support the standard families) since it should have been an extension to ClientBehaviourHolders. Maybe there is a way to wrap arround existing components but i haven't found it yet.

private static final String FAMILY = "jquery";

@Override
public String getFamily() {
return FAMILY;
}

Because we are a ClientBehaviourHolder, we need to specify the events we support, and what the default event is. In the normal tags, these are "hardcoded" into the tags themself and are limited to the official dom events for each tag.Due to the way JQuery works ,adding custom events, and using bind(...); this imposes a limitation. We will create a tag that will allow use to reuse it for different jquery plugins and we will do this by allowing the supported events to be specified on the tag itself: [Note: Thinking about it now, space seperated list would be better since that is the default modus of the JSF tags. ]

@Override
public Collection getEventNames() {
return Arrays.asList(((String) getAttributes().get("events")).split(","));
}

@Override
public String getDefaultEventName() {
return (String) getAttributes().get("default");
}

Al that is left is look at the two big methods that seem to do all the work. I didn't write them myself, they are based on an entry by Jim Driscoll over here. He explains the encode and decode in details so, go to his blog and read it over there if you can't figure it out yourself.

Some things will need a bit of explanation. first of all we have the following line:

String jQueryId = clientId.replace(":", "\\\\:");

Every web developper and designer knows that ":" is a pseudo selector in CSS. So it's not a good idea to use it in id's. But well, for historical reasons i hope, JSF does use as a separator in NamingContainer components. The trick is to escape it for JQuery, by outputing extra slashes, which need to be escaped for java. Ow, and we need to escape it for javascript too... so '\:'(jquery) becomes '\\:' (javascript) becomes '\\\\:'(java).

Then we need a script tag in the pages itself where we will output everything we need.

ResponseWriter responseWriter = context.getResponseWriter();
responseWriter.startElement("script", null);
responseWriter.writeAttribute("type", "text/javascript", null);
....
responseWriter.endElement("script");

In this script tag we will need to output JQuery code to bind an action to the event specified by the f:ajax tag, and call whatever javascriptcode jsf normaly calls when we are using the default events. We want javascript code that looks like this:

<script type="text/javascript">
$('#clientId').bind('eventname', function(event, ui) { 
	wheteveJSFcallsHere(); 
});
//repeat for all events ...
</script>

We can base it on the code by Jim Driscoll.

private static final String TEMPLATE_EVENT = "\n$('#%s').bind('%s', function(event, ui) { %s; });\n";

Map> behaviors = getClientBehaviors();
for (String eventName : behaviors.keySet()) {
    if (getEventNames().contains(eventName)) {

        ClientBehaviorContext behaviorContext = ClientBehaviorContext
                .createClientBehaviorContext(context, this, eventName, clientId, null);

        String jsfJsCode = behaviors.get(eventName)
                .get(0).getScript(behaviorContext);

        String jsCode = String.format(TEMPLATE_EVENT, jQueryId, eventName, jsfJsCode);

        responseWriter.writeText(jsCode, null);
    }

}

According to the specification, the default f:ajax returns inline javascript as specified in the documentation. We can get it by using this line:

behaviors.get(eventName).get(0).getScript(behaviorContext);

To complete it, and allow us to specify a plugin for out tags, we add the two extra attributes, plugin and options which we use to generate another line of javascript code:

private static final String TEMPLATE_PLUGIN = "\n$('#%s').%s(%s);\n";

String plugin = (String) getAttributes().get("plugin");
if(null != plugin) {
    String options = (String) getAttributes().get("options");
    if(options == null) options = "";
    String jsCode = String.format(TEMPLATE_PLUGIN, jQueryId, plugin, options);
    responseWriter.writeText(jsCode, null);
}

On last thin in this class is defining ResourceDependencies so that whenever this component is used, the needed javascript files are inserted. We do this by adding the following annotations:

@ResourceDependencies({ @ResourceDependency(name = "jsf.js", target = "head", library="javax.faces"), @ResourceDependency(name = "jquery.js", target = "head"), @ResourceDependency(name = "jquery-ui.js", target = "head") })

To make this work, the jquery.js and jquery-ui.js files need to be in the 'META-INF/resources' folder.

Now, in order to use this tag in our facelets page we need a taglib.xml file, I named it howest.taglib.xml with the following content.

<?xml version="1.0" encoding="UTF-8"?>
<facelet-taglib xmlns="http://java.sun.com/xml/ns/javaee"
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-facelettaglibrary_2_0.xsd"
                version="2.0">
    <namespace>http://howest.be/jsf2/v1</namespace>
    <tag>
        <tag-name>jquery</tag-name>
        <component>
            <component-type>jquery</component-type>
        </component>
    </tag>
</facelet-taglib>

Last step, register it in the web.xml by adding a context parameter.

<context-param>
	<param-name>javax.faces.FACELETS_LIBRARIES</param-name>
	<param-value>/WEB-INF/howest-taglib.xml</param-value>
</context-param>

So there you have it, a base jquery component you can use to create composite components that are able to use jquery and jquery plugins