|
Ajax
Components
Ajax is cool, but implementing—and
especially
reimplementing
and debugging—low-level Ajax code is not cool. To rid
ourselves
of that
burden
entirely, we now
turn
to JSF custom components, which happen to be an
excellent
vehicle for encapsulating Ajax code. Once encapsulated, our custom components can be used via JSP tags to create compelling
user
experiences.
Hybrid Components
It should be
fairly
obvious that the road to Ajax bliss can be paved by implementing custom renderers that emit JavaScript code.
Even more interesting, however, are JSF components that wrap existing Java-Script components. After all, why would you want to implement components, such as accordions or drag-and-drop, from scratch when you have a wide variety of existing components to choose from: Prototype, Scriptaculous, Dojo, and Rico? Wrapping those components with JSF components, so that you can use them in your JSF applications, is a straightforward task.
The Rico Accordion
Rico is a one of a number of frameworks based on Prototype. Rico provides amenities such as drag-and-drop and a handful of useful components. One of those components is an accordion, in the Flash tradition, shown in Figure 11-7.
The Rico accordion component is similar to a tabbed pane with fancy transitions—when you click the header of an accordion panel, the header animates either up or down to reveal its associated panel. Here is how you implement the accordion, shown in Figure 11-7, using HTML:
<html> <head> <link href="styles.css" rel="stylesheet" type="text/css"/> <script type='text/javascript' src='prototype.js'></script> <script type='text/javascript' src='rico-1.1.2.js'></script> <script type='text/javascript'> function createAccordion() {
new Rico.Accordion($("theDiv"));
} </script> </head> <body
onload="createAccordion();
"> <div id="
theDiv
" class="accordion"> <div class="accordionPanel"> <div class="accordionPanelHeader"> Fruits </div> <div class="accordionPanelContent"> <ul> <li>Oranges</li> <li>Apples</li> <li>Watermelon</li> <li>Kiwi</li> </ul> </div> </div> <div class="accordionPanel"> <div class="accordionPanelHeader"> Vegetables </div> <div class="accordionPanelContent"> <ul> <li>Radishes</li> <li>Carrots</li> <li>Spinach</li> <li>Celery</li> </ul> </div> </div> </div> </body> </html>
When the
preceding
page loads, Rico creates an instance of
Rico.Accordion
, which adds behaviors to the DIV that it's passed. In this case, Rico endows the DIV with JavaScript event handlers that
react
to mouse clicks in the header of each panel.
In the
next
section, we see how to wrap the Rico accordion in a JSF component.
The JSF-Rico Accordion Hybrid
The application shown in Figure 11-8 is a hybrid component, meaning a JSF component that wraps a JavaScript component. In this case, that JavaScript component is a Rico accordion component.
The Rico component automatically adds a scrollbar if the content of a panel overflows the
size
of the panel, so we get that functionality for free. As Figure 11-9 illustrates, you can put anything you want in an accordion panel, including forms.
Using the accordion component is simple:
<html> <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %> <%@ taglib uri="http://java.sun.com/jsf/
core
" prefix="f"%>
<%@ taglib uri="http://corejsf/rico" prefix="rico"%>
<f:view> ... <rico:
accordion
name
="bookAccordion" panelHeight="175" styleClass="accordion" panelClass="accordionPanel" headerClass="accordionPanelHeader" contentClass="accordionPanelContent"> <rico:
accordionPanel
heading="#{msgs.whatIsAHybrid}"> <
jsp:include
page="/whatIsAHybrid.jsp"/> </rico:accordionPanel> <rico:accordionPanel heading="#{msgs.aboutThisComponent}"> <jsp:include page="/aboutTheAccordion.jsp"/> </rico:accordionPanel> <rico:accordionPanel heading="#{msgs.aboutRico}"> <jsp:include page="/aboutRico.jsp"/> </rico:accordionPanel> <rico:accordionPanel heading="#{msgs.lookWhatYouCanDo}"> <jsp:include page="/lookWhatYouCanDo.jsp"/> </rico:accordionPanel> </rico:accordion> ... </f:view> </html>
The
rico:accordion
and
rico:accordionPanel
tags represent custom renderers that we pair with
UICommand
components. Those renderers generate the Rico-aware Java-Script that creates the Rico accordion.
The Rico-aware renderers do two things you may find useful if you decide to implement JSF components with Ajax capabilities of your own: keeping your JavaScript separate from your renderers and transmitting JSP tag attributes to that JavaScript code.
Keeping JavaScript Out of Renderers
One thing quickly becomes apparent if you start implementing Ajax-enabled custom components: You do not want to generate JavaScript with
PrintWriter. write
statements. It is much easier to maintain JavaScript if it is in a file of its own. Finally, it is
convenient
to colocate JavaScript files with the Java classes that use them. Let's see how we can do those things.
The
AccordionRenderer
class generates a
script
element whose
src
attribute's value is
rico-script.jsf
:
public class AccordionRenderer extends Renderer {
public void encodeBegin(FacesContext fc,
UIComponent component)
throws IOException {
ResponseWriter writer = fc.getResponseWriter();
// write the script for loading the Rico JS file
writer.write("<script type='text/javascript'"
+ "src='
rico-script.jsf
'>"
+ "</script>");
...
}
}
That
src
attribute—
rico-script.jsf
—results in a call to the server with the URL
rico-script.jsf
. That URL is handled by a phase listener:
public class AjaxPhaseListener implements PhaseListener { private static final String RICO_SCRIPT_REQUEST = "
rico-script
"; private static final String PROTOTYPE_SCRIPT_FILE = "prototype.js"; private static final String SCRIPTACULOUS_SCRIPT_FILE = "scriptaculous.js"; private static final String RICO_SCRIPT_FILE = "rico-1.1.2.js"; public PhaseId getPhaseId() { // We need access to the view state return PhaseId.
RESTORE_VIEW;
// in afterPhase() } public void beforePhase(PhaseEvent phaseEvent) { // not interested } public void
afterPhase
(PhaseEvent phaseEvent) { // After the RESTORE VIEW phase FacesContext fc = FacesContext.getCurrentInstance(); if(((HttpServletRequest)fc.getExternalContext() .getRequest()).getRequestURI() .contains(RICO_SCRIPT_REQUEST)) { try {
readAndWriteFiles
(fc, phaseEvent, new String[] { PROTOTYPE_SCRIPT_FILE, SCRIPTACULOUS_SCRIPT_FILE, RICO_SCRIPT_FILE }); } catch(java.io.IOException ex) { ex.printStackTrace(); }
phaseEvent.getFacesContext().responseComplete();
} } private void
readAndWriteFiles
(FacesContext fc, PhaseEvent pe, STring[] files) { // Read files and write them to the response } }
If the request URI contains the string
rico-script
, the phase listener reads three files and
writes
them to the response:
prototype.js
,
scriptaculous.js
, and
rico-1.1.2.js
.
Realize that this roundabout way of reading JavaScript files could be avoided by simply specifying the files
themselves
in the script element generated by the accordion renderer; however, that would require us to
hardcode
the location of that file. Because we have used a phase listener to load the JavaScript files, we can colocate those JavaScript files with the phase listener, without having to explicitly specify the JavaScript file locations in the JSP pages.
Transmitting JSP Tag Attributes to JavaScript Code
If you implement Ajax-enabled JSF components, you will most likely need to transfer tag attributes, specified in a JSP page, to JavaScript that is stored in a file of its own, as described in "Keeping JavaScript Out of Renderers" on page 551. Let's see how that is done with the accordion component.
First, the accordion tag class provides setters and getters, which are called by JSP, for its tag attribute values. After JSP transmits tag attribute values to tag properties, JSF calls the tag's
setProperties
method, which
passes
those attribute values through to the component.
public class AccordionTag extends UIComponentELTag { private ValueExpression
name
= null; ... public void
setName
(ValueExpression name) { // Called by JSP this.name = name; } ... protected void
setProperties
(UIComponent component) { // Called by JSF ... component.setValueExpression("name", name); ... } }
When the component is rendered, the accordion renderer obtains the tag values from the component and generates a small snippet of JavaScript that passes the component values through to the JavaScript; in this case, we are passing the name of the DIV that Rico will endow with accordion functionality. That DIV was originally specified as the
name
attribute of the
rico:accordion
tag.
public class AccordionRenderer extends Renderer { ... public void encodeEnd(FacesContext fc, UIComponent component) throws IOException { ResponseWriter writer = fc.getResponseWriter(); // Finish enclosing DIV started in encodeBegin() writer.write("</div>"); // Write the JS that creates the Rico Accordian component Map accordionAttributes =
component.getAttributes(); String div = (String)accordionAttributes.get("name");
writer.write("<script type='text/javascript'>"); writer.write("new Rico.Accordion( $('" + div + "'), "); writeAccordionAttributes(writer, accordionAttributes); writer.write(");"); writer.write("</script>"); } public boolean getRendersChildren() { return false; } private void writeAccordionAttributes(ResponseWriter writer, Map attrs) { try { // Add the rest of the Accordion's properties here. // See rico-1.1.2.js, line 179. writer.write(" { "); writer.write(" panelHeight: " + attrs.get("panelHeight")); writer.write(" } "); } catch (IOException e) { e.printStackTrace(); } } }
|