Adding AJAX support to JSF custom components

In this recipe, we get to the next level and we will create a much complex custom component. Step by step, we will build an image slide viewer with AJAX functionality.

Remember that we will consider the ideas from the previous two recipes to be already known, therefore it is mandatory to read them first!

Getting ready

We have developed this recipe with NetBeans 6.8, JSF 2.0, and GlassFish v3. The JSF 2.0 classes were obtained from the NetBeans JSF 2.0 bundled library. In addition, we have used the Dynamic Faces project, which provides support for JSF 2.0 and extends the JSF lifecycle to work on AJAX requests. You can download this distribution from https://jsf-extensions.dev.java.net/. The Dynamic Faces libraries (including necessary dependencies) are in the book code bundle, under the /JSF_libs/Dynamic Faces JSF 2.0 folder.

How to do it...

Our recipe will have three stages. In the first stage, our component will be a simple image viewer. In the next stage, it will be an image slide viewer, and in the final stage, it will become an image slide viewer with AJAX functionality.

Stage 1—creating an image viewer

To begin with we develop the component class. This time we render an image to the client, therefore our component will extend the UIOutput component, as shown next (the picture is characterized by three attributes—width (image width), height (image height), and path (image URL)):

package custom.component;
import javax.faces.component.UIOutput;
public class UIImageOutput extends UIOutput {
private static final String IMAGE_FAMILY = "IMAGE_FAMILY";
private String width;
private String height;
private String path;
public String getHeight() {
return height;
}
public void setHeight(String height) {
this.height = height;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getWidth() {
return width;
}
public void setWidth(String width) {
this.width = width;
}
public UIImageOutput() {
super();
}
@Override
public String getFamily() {
return IMAGE_FAMILY;
}
}

Next, we implement the tag handler class. There is nothing special to it, therefore we can write it right away:

package custom.component;
import javax.faces.component.UIComponent;
import javax.faces.webapp.UIComponentELTag;
public class UIImageOutputTag extends UIComponentELTag {
private static final String IMAGE_OUTPUT = "IMAGE_OUTPUT";
private static final String IMAGE_RENDERER = "IMAGE_RENDERER";
private String width;
private String height;
private String path;
public String getHeight() {
return height;
}
public void setHeight(String height) {
this.height = height;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getWidth() {
return width;
}
public void setWidth(String width) {
this.width = width;
}
public String getComponentType() {
return IMAGE_OUTPUT;
}
public String getRendererType() {
return IMAGE_RENDERER;
}
@Override
protected void setProperties(UIComponent ui_comp) {
super.setProperties(ui_comp);
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
if (path != null) {
uiImageOutput.setPath(path);
}
if (width != null) {
uiImageOutput.setWidth(width);
}
if (height != null) {
uiImageOutput.setHeight(height);
}
}
}

Finally, we must create a custom renderer for our component. Obviously, we need only the encodeBegin method, therefore our job becomes easy:

package custom.component;
import java.io.IOException;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import javax.servlet.ServletContext;
public class UIImageOutputRenderer extends Renderer{
@Override
public void encodeBegin(FacesContext ctx, UIComponent ui_comp) throws
IOException {
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
ResponseWriter responseWriter = ctx.getResponseWriter();
responseWriter.startElement("div",ui_comp);
String width = uiImageOutput.getWidth();
String height = uiImageOutput.getHeight();
ServletContext servletContext =
(ServletContext) ctx.getExternalContext().getContext();
String contextPath = servletContext.getContextPath();
responseWriter.startElement("img", uiImageOutput);
responseWriter.writeAttribute("src",
contextPath + uiImageOutput.getPath(), "path");
responseWriter.writeAttribute("width", width, "width");
responseWriter.writeAttribute("height", height, "height");
responseWriter.endElement("div");
}
}

At the configuration level, we need to add the component and the renderer in the faces-config.xml file:

<?xml version='1.0' encoding='UTF-8'?>
<faces-config version="1.2"
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-facesconfig_1_2.xsd">
<component>
<component-type>IMAGE_OUTPUT</component-type>
<component-class>custom.component.UIImageOutput</component-class>
</component>
<render-kit>
<renderer>
<description>
Renderer for the image component.
</description>
<component-family>IMAGE_FAMILY</component-family>
<renderer-type>IMAGE_RENDERER</renderer-type>
<renderer-class>
custom.component.UIImageOutputRenderer
</renderer-class>
</renderer>
</render-kit>
</faces-config>

Now, it is time to test our component, and for this we wrote the following view (JSP page):

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
<%@taglib prefix="e" uri="http://packt.net/cookbook/components"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<f:view>
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=UTF-8"/>
<title>JSF image viewer custom component</title>
</head>
<body>
<h3><h:outputText value="This image is provided
by a JSF custom component:"/></h3>
<h:form>
<e:imgOutput path="/img/rafa_1.jpg"
width="340" height="466" />
</h:form>
</body>
</html>
</f:view>

The output is shown next:

Stage 1—creating an image viewer

Stage 2—transforming the image viewer into an image slide viewer

We continue to extend our previous component to become an image slide viewer. In the end, the component will display one image at a time, and will have two buttons for navigating to the next/previous image. The images will be specified in the path attribute separated by a comma, as shown next:

<e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg, /img/rafa_3.jpg,
/img/rafa_4.jpg, /img/rafa_5.jpg" width="340" height="466" />

Now, let's see the modifications that we should accomplish. To begin with, we modify the component class by adding two more properties, one for holding the image count (we name it imgIndex) and one for storing image URLs (we name it paths). In addition, in this class, we will override two more methods—saveState and restoreState. These methods are responsible for preserving the state of the component. Now, the component class is:

package custom.component;
import javax.faces.component.UIOutput;
import javax.faces.context.FacesContext;
public class UIImageOutput extends UIOutput {
private static final String IMAGE_FAMILY = "IMAGE_FAMILY";
private String width;
private String height;
private String path;
private String[] paths;
private int imgIndex;
public int getImgIndex() {
return imgIndex;
}
public void setImgIndex(int imgIndex) {
this.imgIndex = imgIndex;
}
public String[] getPaths() {
return paths;
}
public void setPaths(String[] paths) {
this.paths = paths;
}
public String getHeight() {
return height;
}
public void setHeight(String height) {
this.height = height;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getWidth() {
return width;
}
public void setWidth(String width) {
this.width = width;
}
public UIImageOutput() {
super();
}
@Override
public Object saveState(FacesContext cxt) {
Object state[] = new Object[5];
state[0] = super.saveState(cxt);
state[1] = paths;
state[2] = new Integer(imgIndex);
state[3] = width;
state[4] = height;
return state;
}
@Override
public void restoreState(FacesContext cxt, Object obj) {
Object state[] = (Object[])obj;
super.restoreState(cxt,state[0]);
paths = (String[])state[1];
imgIndex = ((Integer)state[2]).intValue();
width = (String)state[3];
height = (String)state[4];
}
@Override
public String getFamily() {
return IMAGE_FAMILY;
}
}

Next, we add a minor but significant modification to the tag handler class. The idea is to split the path attribute content, using the comma delimiter, to extract the images paths. Here is the new setProperties method:

…
@Override
protected void setProperties(UIComponent ui_comp) {
super.setProperties(ui_comp);
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
if (path != null) {
String[] imgPaths = path.trim().split(",");
uiImageOutput.setPath(imgPaths[0]);
uiImageOutput.setPaths(imgPaths);
}
if (width != null) {
uiImageOutput.setWidth(width);
}
if (height != null) {
uiImageOutput.setHeight(height);
}
}
…

The last modification is also the most consistent one. We adapt the component renderer for rendering HTML and JavaScript. When the client presses the navigation buttons, the component should trigger the onClick mouse event. The JavaScript associated with the onClick mouse event submits the form. In addition, we need a hidden field to hold the information provided by the JavaScript about the clicked button. A JavaScript snippet is shown next (this is copied from browser's page source):

<script type="text/javascript">
var j_id_id28j_id_id30_F = document.forms['j_id_id28'];
function j_id_id30_PB(element) {
if (j_id_id28j_id_id30_F.onsubmit == null ||
j_id_id28j_id_id30_F.onsubmit()) {
j_id_id28j_id_id30_F.j_id_id28_j_id_id30_H.value = element.id;
j_id_id28j_id_id30_F.submit();
}
}
</script>

For implementing this we need four methods as follows:

private UIForm getUIForm(UIComponent ui_comp) {
UIComponent uiParent = ui_comp.getParent();
if (uiParent == null) {
throw new IllegalStateException("Form unavailable!");
}
while (uiParent != null) {
if (uiParent instanceof UIForm) {
break;
}
uiParent = uiParent.getParent();
}
return (UIForm) uiParent;
}
private String previousLink(FacesContext ctx, UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_P";
return result;
}
private String nextLink(FacesContext ctx, UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_N";
return result;
}
private String hiddenField(FacesContext ctx,UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_H";
return result;
}

Finally, we need to modify the encodeBegin method and implement the decode method, as shown next (the decode method will take care the index value of paths relative to the hidden field value and set the path property based on the index value):

package custom.component;
import java.io.IOException;
import java.util.Map;
import javax.faces.component.UIComponent;
import javax.faces.component.UIForm;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.faces.render.Renderer;
import javax.servlet.ServletContext;
public class UIImageOutputRenderer extends Renderer{
private UIForm getUIForm(UIComponent ui_comp) {
UIComponent uiParent = ui_comp.getParent();
if (uiParent == null) {
throw new IllegalStateException("Form unavailable!");
}
while (uiParent != null) {
if (uiParent instanceof UIForm) {
break;
}
uiParent = uiParent.getParent();
}
return (UIForm) uiParent;
}
private String previousLink(FacesContext ctx,
UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_P";
return result;
}
private String nextLink(FacesContext ctx, UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_N";
return result;
}
private String hiddenField(FacesContext ctx,
UIComponent ui_comp){
String clientId = getUIForm(ui_comp).getId();
String uiClientId = ui_comp.getId();
String result = clientId + "_" + uiClientId + "_H";
return result;
}
@Override
public void encodeBegin(FacesContext ctx,
UIComponent ui_comp) throws IOException {
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
ResponseWriter responseWriter = ctx.getResponseWriter();
responseWriter.startElement("table", uiImageOutput);
// get "id" attribute
String id = (String)uiImageOutput.getClientId(ctx);
responseWriter.writeAttribute("id", id, null);
//Java Script postback code
UIForm uiForm = getUIForm(uiImageOutput);
String clientId = uiForm.getClientId(ctx);
String postBack = uiImageOutput.getId() + "_PB";
String formName = uiForm.getId() + uiImageOutput.getId() + "_F";
responseWriter.startElement("script", uiImageOutput);
responseWriter.writeAttribute("type", "text/javascript", null);
String script = "
var " + formName + " = document.forms['" + clientId + "'];" + "
function" + " " + postBack + "(element) {
" +
" if (" + formName + ".onsubmit == null || " + formName + ".onsubmit()) {
" + " " + formName + "." + hiddenField(ctx, uiImageOutput) +
".value = element.id; 
" + " " + formName + ".submit();" + "
 } 
} 
";
responseWriter.writeText(script, ui_comp, null);
responseWriter.endElement("script");
responseWriter.startElement("input", uiImageOutput);
responseWriter.writeAttribute("type", "hidden", null);
responseWriter.writeAttribute("name",
hiddenField(ctx, uiImageOutput), null);
responseWriter.writeAttribute("value", "", null);
responseWriter.endElement("input");
// "tr" element
responseWriter.startElement("tr", uiImageOutput);
// "td" element (image)
responseWriter.startElement("td", uiImageOutput);
// Render the image
ServletContext servletContext = (ServletContext)ctx.
getExternalContext().getContext();
String contextPath = servletContext.getContextPath();
responseWriter.startElement("img", uiImageOutput);
responseWriter.writeAttribute("src",
contextPath + uiImageOutput.getPath(), "url");
responseWriter.writeAttribute("width",
uiImageOutput.getWidth(), "width");
responseWriter.writeAttribute("height",
uiImageOutput.getHeight(), "height");
responseWriter.endElement("td");
responseWriter.endElement("tr");
// "tr" element
responseWriter.startElement("tr", uiImageOutput);
// "td" element (links)
responseWriter.startElement("td", uiImageOutput);
// Previous image link
responseWriter.startElement("input", uiImageOutput);
responseWriter.writeAttribute("type", "button" , null);
responseWriter.writeAttribute("value", "Previous" , null);
responseWriter.writeAttribute("onClick",
"javascript:" + postBack + "(this)", null);
responseWriter.writeAttribute("id",
previousLink(ctx, ui_comp), null);
responseWriter.endElement("input");
// Next image link
responseWriter.startElement("input", uiImageOutput);
responseWriter.writeAttribute("type", "button" , null);
responseWriter.writeAttribute("value", "Next" , null);
responseWriter.writeAttribute("onClick",
"javascript:" + postBack + "(this)", null);
responseWriter.writeAttribute("id",
nextLink(ctx, ui_comp), null);
responseWriter.endElement("input");
responseWriter.endElement("td");
responseWriter.endElement("tr");
responseWriter.endElement("table");
}
@Override
public void decode(FacesContext ctx, UIComponent ui_comp) {
if ((ctx == null) || (ui_comp == null))
{ throw new NullPointerException(); }
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
String hidden_field = hiddenField(ctx, uiImageOutput);
Map paramsMap = ctx.getExternalContext().
getRequestParameterMap();
String valH = (String)paramsMap.get(hidden_field);
String[] img_paths = uiImageOutput.getPaths();
int img_index = uiImageOutput.getImgIndex();
if (valH.equals(previousLink(ctx, ui_comp))){
if (img_index > 0){
img_index = img_index-1;
uiImageOutput.setImgIndex(img_index);
}
}else if (valH.equals(nextLink(ctx, ui_comp))){
if (img_index < img_paths.length - 1){
img_index = img_index+1;
uiImageOutput.setImgIndex(img_index);
}
}
uiImageOutput.setPath(img_paths[img_index]);
}
}

Finally, we modify the JSP page that uses our component as shown next:

<e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg, /img/rafa_3.jpg,
/img/rafa_4.jpg, /img/rafa_5.jpg" width="340" height="466" />

Now, you can test the application again!

Stage 3—adding AJAX capabilities to the image slide viewer component

We continue by adding AJAX capabilities to our image slide viewer. For this, we will use the Dynamic Faces project, which extends the JSF lifecycle to work on AJAX requests. After you have downloaded Dynamic Faces from https://jsf-extensions.dev.java.net/ and placed the libraries in your project, you must accomplish a set of modifications to enable AJAX on this custom component.

We start with a configuration task that should be accomplished in the web.xml descriptor. Add the following lines to the Faces Servlet:

<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<!-- For Dynamic Faces -->
<init-param>
<param-name>javax.faces.LIFECYCLE_ID</param-name>
<param-value>com.sun.faces.lifecycle.PARTIAL</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>

Next, modify the encodeBegin method of the renderer class, as shown next:

@Override
public void encodeBegin(FacesContext ctx, UIComponent ui_comp) throws
IOException {
UIImageOutput uiImageOutput = (UIImageOutput)ui_comp;
ResponseWriter responseWriter = ctx.getResponseWriter();
responseWriter.startElement("table", uiImageOutput);
// get "id" attribute
String id = (String)uiImageOutput.getClientId(ctx);
responseWriter.writeAttribute("id", id, null);
//Java Script postback code
UIForm uiForm = getUIForm(uiImageOutput);
String clientId = uiForm.getClientId(ctx);
String postBack = uiImageOutput.getId() + "_PB";
String formName = uiForm.getId() + uiImageOutput.getId() + "_F";
responseWriter.startElement("script", uiImageOutput);
responseWriter.writeAttribute("type", "text/javascript", null);
//with AJAX
String script = "
var " + formName + " = document.forms['" + clientId + "'];" + "
function" + " " + postBack + "(element) {
" +
" if (" + formName + ".onsubmit == null || " + formName + ".onsubmit()) {
" + " document.getElementById('" + hiddenField(ctx, uiImageOutput) +
"').value = element.id; 
" + " DynaFaces.fireAjaxTransaction(element,{execute:'" + id + "',render:'" + id + "',inputs:'" +
hiddenField(ctx, uiImageOutput) + "'});" + "
}
}
";
responseWriter.writeText(script, ui_comp, null);
responseWriter.endElement("script");
responseWriter.startElement("input", uiImageOutput);
responseWriter.writeAttribute("type", "hidden", null);
//with AJAX
responseWriter.writeAttribute("id",
hiddenField(ctx, iImageOutput), null);
responseWriter.writeAttribute("value", "", null);
responseWriter.endElement("input");
…

Finally, modify the JSP page to add Dynamic Faces taglib, and add the <jsfExt:scripts> tag to the <head>, as shown next:

<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="f" uri="http://java.sun.com/jsf/core"%>
<%@taglib prefix="h" uri="http://java.sun.com/jsf/html"%>
<%@taglib prefix="e" uri="http://packt.net/cookbook/components"%>
<%@taglib prefix="jsfxt" uri="http://java.sun.com/jsf/extensions/dynafaces"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<f:view>
<html>
<head>
<meta http-equiv="Content-Type"
content="text/html; charset=UTF-8"/>
<title>JSF custom component, AJAX enabled</title>
<jsfxt:scripts />
</head>
<body>
<h3><h:outputText value="This images are
provided by a JSF custom component AJAX enabled:"/></h3>
<h:form>
<e:imgOutput path="/img/rafa_1.jpg, /img/rafa_2.jpg,
/img/rafa_3.jpg, /img/rafa_4.jpg, /img/rafa_5.jpg"
width="340" height="466" />
</h:form>
</body>
</html>
</f:view>

Test the application again, and notice how AJAX is getting into the equation!

See also

The code bundled with this book contains a complete example of this recipe. The project can be opened with NetBeans 6.8 and it is named: AJAX_support_for_custom_components.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset