Creating a Custom JSF 1.2 Component - With Facets, Resource Handling, Events and Listeners
I occasionally create custom JavaServer Faces components. Just enough to sort of remember what the steps are, but not nearly frequently enough to quickly put a new component together. This article demonstrates the quick step approach to creating a new custom component in the old fashioned way (that means: it is not a Facelets template based or an ADF Faces 11g Declarative Component). Its primary purpose is to help me quickly retrace my steps. But perhaps it will benefit some of you as well. The Shuffler component I will develop supports facets. It will render its facet children - one after the other. Which one is rendered first can be indicated through an attribute facetOrder (values normal, reverse and random), which is EL enabled. A shuffler-method-expression can optionally be set to provide the Shuffler with a shuffle-order-processor: the method is invoked with the list of facets to shuffle and will return it in the order in which to render the children. The component can render with a shuffle icon that when pressed causes the children to be shuffled. The Shuffler component allows registration of Shuffle Event Listeners, custom listeners that are informed whenever the shuffle event occurs. An example of how the Shuffler can be used inside a JSF page: Some elements of custom JSF components that are explicitly discussed in this article: dynamic attributes of type ValueExpression (EL enabled) attributes of type MethodExpression (also EL enabled) facets (custom) events and listeners Bare essentials for custom JSF components A custom JSF component is represented by a Java Class - one that extends from UIComponentBase. An instance of this class is created whenever a new page is rendered that contains the component (and for each occurrence of the component in the page, a new instance of the class is created). The component class holds the attributes that are set by the page developer and that determine the behavior and appearance of the component. The component class has the internal logic of the component and it deals for example with events and listeners. This class may also render the markup (HTML) for the component - though it is a better practice to leave the actual rendering to a Renderer class. public class Shuffler extends UIComponentBase { ... } Most custom JSF component will also have an associated Renderer class, that extends from Renderer. Note that some components will not actually be rendered (such as Listeners, Iterators or Parameters) and therefore will not have a Renderer class. The Renderer is not only responsible for rendering the HTML, it will also inspect (decode) the incoming request from the browser to see whether the request parameter map contains values that are of interest to the component - that indicate for example that a value has been entered or set on the component(’s representation in the browser) or an action has been executed against it. Note that one JSF component may have multiple Renderers, for example for different channels and protocols (to render a representation of the component in plain XML, in WML, in JavaFX or XUL) or for different user agents (Firefox, Internet Explorer) or themes (professional user, internet surfer). public class ShufflerRenderer extends SuperRenderer { ... } JavaServer Faces pages can be created in various ways - including programmatically, using Facelets and using JSP pages. The latter option, through JSP, is still the most common one, though that is about to change with JSF 2.0 favoring Facelets. Page developers using JSPs will describe the JSF component tree that will need to be instantiated in memory for rendering a certain View using a plain JSP page. The tags in the JSP page are normal JSP tags - described by TLD (tag library descriptors) - corresponding to JSF components and therefore JSF component classes. Every JSF component that needs to be used in JSPs has to have a corresponding JSP tag-class, one that will typically extend from UIComponentELTag (or just from TagSupport when no JSF component is added to the component tree for a certain tag, for example when that tag represents a listener or parameter). The Tag Class specifies which JSF Component it represents. It also indicates which Renderer should be used to render the component. This means that one component can have multiple JSP tags associated with it, each providing a different way of rendering the component. Note: the renderer can also be specified dynamically - taking user preferences or characteristics into account public class ShufflerTag extends UIComponentELTag { public static final String COMPONENT_TYPE = "nl.amis.jsf.UIShuffler"; public static final String RENDERER_TYPE = "nl.amis.jsf.ShufflerRenderer"; public String getComponentType() { return COMPONENT_TYPE; } public String getRendererType() { return RENDERER_TYPE; } ... } Tags representing JSF components need to be described in TLD files (Tag Library Descriptors) just like any other JSP tag.The entry in the TLD defines the tag label to use in the page, whether the tag can contain child-tags, some descriptive meta data and every attribute that can be configured in the tag. For each attribute the TLD-entry specifies the type, whether it is required and if the attribute can contain an EL expression passing in a value or an EL expression passing in a method; in the latter case, the entry also prescribes the signature of the method: ShufflerLib 1.0 ShufflerLib /nl.amis,jsf/ShufflerLib Writes a DIV element that contains the facets in a specific order. shuffler nl.amis.jsf.shuffler.ShufflerTag JSP id false true rendered false boolean binding false javax.faces.component.UIComponent styleClass false java.lang.String .... JSF components need to be registered in a special faces-config.xml file (special in the sense that it is not the faces-config.xml that drives a web application but rather one that acts like a repository of components and their renderers. Note however that all entries in this special faces-config.xml is merged together with the ‘normal’ faces-config.xml. That means in turn that while the special file is primarily seen as the registry of components, it can also configure PhaseListeners, Navigation Rules (hard to see the value in that) and Managed Beans (which can be very useful). The component registration in faces-config.xml consists of a component type that is associated with a the component class. nl.amis.jsf.UIShuffler nl.amis.jsf.shuffler.Shuffler Renderers can also be registered in this file. A renderer entry registers a renderer-type (corresponding to the value returned by the getRendererType() method in the tag class) associated with the RendererClass. Based on the value (rendererType) returned by the tag class, the correct class to instantiate can be determined from this entry: nl.amis.Shuffler nl.amis.jsf.ShufflerRenderer nl.amis.jsf.shuffler.ShufflerRenderer Implementing the Classes: Component, Renderer and TagHandler The TagHandler ShufflerTag is the intermediary between the world of JSP pages (and the Servlet/JSP engine that translates the JSP file into a servlet class) and the JSF realm. Every tag in the JSP page needs to be turned into its corresponding JSF representation. The tag handler needs to override the setProperties() method inherited from the UIComponentELTag class; this method takes all the values set on the tag attributes in the page and passes them onwards to the Component. In our initial case, the tag is used in JSPs like this: ... other content The styleClass attribute is the only one we defined - id and rendered are defined on every JSP-tag based on JSF’s UIComponentELTag. Thye styleClass attribute is also the only attribute we need to take responsibility for in the tag class, by providing a setter method that sets a private member and by passing the value of that private member to the component in the setProperties() method. The code for the ShufflerTag class now becomes: package nl.amis.jsf.shuffler; import javax.el.ValueExpression; import javax.faces.component.UIComponent; import javax.faces.webapp.UIComponentELTag; public class ShufflerTag extends UIComponentELTag { public static final String COMPONENT_TYPE = "nl.amis.jsf.UIShuffler"; public static final String RENDERER_TYPE = "nl.amis.jsf.ShufflerRenderer"; private ValueExpression styleClass; public String getComponentType() { return COMPONENT_TYPE; } public String getRendererType() { return RENDERER_TYPE; } protected void setProperties(UIComponent component) { super.setProperties(component); processProperty(component, styleClass, Shuffler.STYLECLASS_ATTRIBUTE_KEY); } public void release() { super.release(); styleClass= null; } protected final void processProperty(final UIComponent component, final ValueExpression property, final String propertyName) { if (property != null) { if(property.isLiteralText()) { component.getAttributes().put(propertyName, property.getExpressionString()); } else { component.setValueExpression(propertyName, property); } } } public void setStyleClass(ValueExpression styleClass) { this.styleClass = styleClass; } } We cater for the fact that styleClass can contain a ValueExpression - as all attributes can, starting from JSF 1.2. In the method processProperty we check whether the string passed for styleClass is a literal string or should be considered an EL expression. In the latter case, we pass a ValueExpression to the component, otherwise a ‘normal’ attribute. Also note that the super class takes care of the attributes id, rendered and binding. However, we do have to specify them in the tag-library. The component class in our case leads a pretty comfortable life: the tag handler informs him of all the attribute values and the actual rendering is left to a special Renderer class. The component is a pretty passive element in this simple example: package nl.amis.jsf.shuffler; import javax.faces.component.UIComponentBase; import javax.faces.context.FacesContext; public class Shuffler extends UIComponentBase { public static final String FAMILY = "nl.amis.Shuffler"; public static final String STYLECLASS_ATTRIBUTE_KEY = "styleClass"; public String getFamily() { return FAMILY; } @Override public Object saveState(FacesContext facesContext) { Object values[] = new Object[2]; values[0] = super.saveState(facesContext); values[1] = this.getAttributes().get(STYLECLASS_ATTRIBUTE_KEY); return values; } @Override public void restoreState(FacesContext facesContext, Object state) { Object values[] = (Object[])state; super.restoreState(facesContext, values[0]); this.getAttributes().put(STYLECLASS_ATTRIBUTE_KEY, values[1]); } } The only really useful thing the component does is implementing the saveState and restoreState methods. These methods play an important part in turning the state of the component into a serializable object array and restoring that state of the component in the RestoreView phase, based on the serialized array. The Tag Handler specifies in its getRendererType() method that the renderer to use for this component when using the shuffler tag, is one called nl.amis.jsf.ShufflerRenderer. In the faces-config.xml file, we have indicated that this renderer type is associated with the class nl.amis.jsf.shuffler.ShufflerRenderer that extends Renderer. The renderers in JSF can override methods like encodeBegin(), encodeEnd(), encodeChildren() and decode() - the latter only when we have to process the incoming request, looking for new values set on or events that occurred on the component. In our case, we initially will simply have the ShufflerRenderer render a DIV element with a class attribute (based on the styleClass attribute). The DIV will allow the children of the Shuffler component to render - by not overriding the encodeChildren() method. package nl.amis.jsf.shuffler; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; public class ShufflerRenderer extends Renderer { @Override public void encodeBegin(final FacesContext facesContext, final UIComponent component) throws IOException { super.encodeBegin(facesContext, component); final ResponseWriter writer = facesContext.getResponseWriter(); writer.startElement("DIV", component); String styleClass = (String)attributes.get(Shuffler.STYLECLASS_ATTRIBUTE_KEY); writer.writeAttribute("class", styleClass, null); } @Override public void encodeEnd(final FacesContext facesContext, final UIComponent component) throws IOException { final ResponseWriter writer = facesContext.getResponseWriter(); writer.endElement("DIV"); } } Next steps - working with facets The Shuffler component is created to dynamically (re)order its child contents. It will do so using facets. The content you want this component to shuffle is passed in two or more facets. The facets are named using string representations of integers, so for example: ... content ... content ... content Facets are automatically supported on JSF components. The getFacets() method is available inside the Shuffler component class and will return a collection of facet UIComponents. Facets are special children for a JSF component: the framework will never render the contents of facets on its own. It is up to the component to determine when and how to render the contents of its facets. So, there is some work to do for the ShuffleRenderer class. But first we need to add support for the new facetOrder attribute. Adding an attribute means: adding an attribute entry in the TLD adding support for processing the attribute in the Tag Handler (a setter and a line of code in setProperties()) adding the attribute in saveState() and restoreState() in the Component class Here we go: In the tld entry, add: facetOrder false java.lang.String In the tag-handler class ShufflerTag add: private ValueExpression facetOrder; public void setFacetOrder(ValueExpression facetOrder) { this.facetOrder = facetOrder; } and in setProperties(): processProperty(component, facetOrder, Shuffler.FACETORDER_ATTRIBUTE_KEY); Finally in the component class Shuffler , add: public static final String FACETORDER_ATTRIBUTE_KEY = "facetOrder"; @Override public Object saveState(FacesContext facesContext) { Object values[] = new Object[3]; values[0] = super.saveState(facesContext); values[1] = this.getAttributes().get(STYLECLASS_ATTRIBUTE_KEY); values[2] = this.getAttributes().get(FACETORDER_ATTRIBUTE_KEY); return values; } @Override public void restoreState(FacesContext facesContext, Object state) { Object values[] = (Object[])state; super.restoreState(facesContext, values[0]); this.getAttributes().put(STYLECLASS_ATTRIBUTE_KEY, values[1]); this.getAttributes().put(FACETORDER_ATTRIBUTE_KEY, values[2]); } The Shuffler also needs to make the facets available to the renderer, in the order that is prescribed by the facetOrder attribute. This attribute supports three values: normal, reverse and random. public List getOrderedFacets(FacesContext facesContext) { // allowable values: normal (default) and reverse // the normal order of the facets is determined by ordering the facets by name (assuming the facetnames are string representations of integers) // create a sorted list with the integers representing the facets List facetIndexValues = new ArrayList(); List facetNames = new ArrayList(getFacets().keySet()); for (String facetName : facetNames) { facetIndexValues.add(new Integer(facetName)); } Collections.sort(facetIndexValues); // create the list of facets corrresponding to the sorted list of facet index values List orderedFacets = new ArrayList(); for (Integer index : facetIndexValues) { orderedFacets.add(getFacets().get(index.toString())); } // depending on the value for the facetOrder attribute, we may need to reorganize the orderedFacets list String facetOrder = (String)this.getAttributes().get(Shuffler.FACETORDER_ATTRIBUTE_KEY); if ("reverse".equalsIgnoreCase(facetOrder)) { Collections.reverse(orderedFacets); } else if ("random".equalsIgnoreCase(facetOrder)) { Collections.shuffle(orderedFacets); } else if ("normal".equalsIgnoreCase(facetOrder)) { // need to do nothing as with normal the order returned by getFacets() is the correct one } return orderedFacets; } The ShufflerRenderer will have to do the real work. It will retrieve the facets - in the proper order - from the Shuffler Component class and ask JSF to render them. package nl.amis.jsf.shuffler; import javax.faces.context.FacesContext; import javax.faces.context.ResponseWriter; import javax.faces.render.Renderer; import javax.faces.component.UIComponent; public class ShufflerRenderer extends Renderer { @Override public void encodeBegin(final FacesContext facesContext, final UIComponent component) throws IOException { super.encodeBegin(facesContext, component); final ResponseWriter writer = facesContext.getResponseWriter(); writer.startElement("DIV", component); String styleClass = (String)attributes.get(Shuffler.STYLECLASS_ATTRIBUTE_KEY); writer.writeAttribute("class", styleClass, null); List orderedFacets = ((Shuffler)component).getOrderedFacets(facesContext); for (UIComponent facet:orderedFacets) { facet.encodeAll(facesContext); } } @Override public void encodeEnd(final FacesContext facesContext, final UIComponent component) throws IOException { final ResponseWriter writer = facesContext.getResponseWriter(); writer.endElement("DIV"); } } With these changes, we can now add real content to the Shuffler and have it rendered, in the order we specified - which can be random. Also note that we can use an EL expression to have the facetOrder dynamically derived: facetOrder="#{bean.liveFacetOrder}" id="s1" > ... content ... content ... content Downloading Resources The next step in our exploration of the development of custom JSF components is the addition of resources like images and JavaScript libraries. Note that in JavaServer Faces 2.0 a new facility is available, especially for this purpose. However, in our 1.2 setting we have to come up with something ourselves. That is not to say no solutions exist for JSF 1.2; almost every library comes with a form of resource handling. Then there is the Weblet framework that was introduced especially for this purpose. Another option leverages JSF itself: its capability through PhaseListeners to intercept a request, interpret the requested ViewId and optionally serve up an image or JS file in response to the request. This approach is proposed in JavaServer Faces, The Complete Reference by Ed Burns and Chris Schalk. I have slightly modified there code for my own purposes. However, the central idea clearly is theirs. My objective is to add an image to the Shuffler component. The next step will be to allow the user to click on the image and by doing so tgrigger a re-shuffle. But that part is for later, first add the image itself. The HTML rendered by the ShufflerRenderer needs to be extended with the IMG tag, that is easy enough. Less trivial is the value for the SRC attribute on the IMG tag. The change in the encodeBegin method in the ShufflerRenderer: writer.startElement("IMG", component); writer.writeAttribute("src", imageUrl( facesContext,SHUFFLE_IMAGE), null); writer.writeAttribute("alt", "Click to reshuffle", null); writer.writeAttribute("width", "20px", null); writer.endElement("IMG"); With SHUFFLE_IMAGE specified as: private static String SHUFFLE_IMAGE = "shuffleIcon.png"; The imageUrl() method is defined as follows private final static String IMAGE_PATH ="/faces/images/"; protected String imageUrl(FacesContext facesContext, String image) { ViewHandler handler = facesContext.getApplication().getViewHandler(); String imageUrl = handler.getResourceURL(facesContext, IMAGE_PATH + image); return imageUrl; } The URLs for images are now constructed to look like this: http://somehost:7101/CustomJSFConsumer/faces/images/shuffleIcon.png The request for the shuffleIcon.png that is sent by the browser should be intercepted by a component that knows how to handle it. Because of the /faces/ part, this request is sent to the FacesServlet and processed through the JSF lifecycle. The componoent to intercept it will be a phaseListener that fires after restore view. It inspects the ViewId. When the ViewId contains the predefined indicator ("/images/") it steps in and takes over processing of the request. It will find the name of the image that is requested by taking the part of the ViewId that comes after /images/. It will then locate the image file on the classpath (that works well for a component packaged in a jar file, it can have the images packaged in the jar file too), looking for a directory called /images/ - as specified by the IMAGE_PATH constant. It copies the image from the file to the outputstream after setting the content type. package nl.amis.jsf; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.URL; import java.net.URLConnection; import javax.faces.context.FacesContext; import javax.faces.event.PhaseEvent; import javax.faces.event.PhaseId; import javax.faces.event.PhaseListener; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; public class ResourceServerPhaseListener implements PhaseListener { public ResourceServerPhaseListener() { super(); } public PhaseId getPhaseId() { return PhaseId.RESTORE_VIEW; } public void afterPhase(PhaseEvent event) { // If this is restoreView phase if (PhaseId.RESTORE_VIEW == event.getPhaseId()) { if (-1 != event.getFacesContext().getViewRoot().getViewId().indexOf(RENDER_IMAGE_TAG)) { // extract the name of the image resource from the ViewId String image = event.getFacesContext().getViewRoot().getViewId().substring(event.getFacesContext() .getViewRoot().getViewId().indexOf(RENDER_IMAGE_TAG) + RENDER_IMAGE_TAG.length()); // render the script writeImage(event, image); event.getFacesContext().responseComplete(); } } } public void beforePhase(PhaseEvent event) { } public static final String RENDER_IMAGE_TAG = "/images/"; public static final String IMAGE_PATH = "/images/"; private void writeImage(PhaseEvent event, String resourceName) { URL url = getClass().getResource(IMAGE_PATH + resourceName); URLConnection conn = null; InputStream stream = null; HttpServletResponse response = (HttpServletResponse)event.getFacesContext().getExternalContext().getResponse(); try { conn = url.openConnection(); conn.setUseCaches(false); stream = conn.getInputStream(); ServletContext servletContext = (ServletContext)FacesContext.getCurrentInstance().getExternalContext().getContext(); String mimeType = servletContext.getMimeType(resourceName); response.setContentType(mimeType); response.setStatus(200); // Copy the contents of the file to the output stream byte[] buf = new byte[1024]; int count = 0; while ((count = stream.read(buf)) >= 0) { response.getOutputStream().write(buf, 0, count); } response.getOutputStream().close(); } catch (Exception e) { String message = null; message = "Can't load image file:" + url.toExternalForm(); try { response.sendError(HttpServletResponse.SC_BAD_REQUEST, message); } catch (IOException f) { f.printStackTrace(); } } } } PhaseListeners need to be configured in order to be active. This configuration usually is done in the faces-config.xml of the application. Fortunately, we can also configure the PhaseListener in the faces-config.xml file that we create for the custom component. This faces-config.xml is part of the jar file in which the custom component is shipped and deployed. Its contents are merged with the application’s own faces-config.xml. The registration of our PhaseListener looks like this: nl.amis.jsf.ResourceServerPhaseListener ... Triggering events on the custom component Time to take another big step. We will support clicking the image by the end user and turn that event into a reshuffle of the facets of the Shuffler component. In the next section we will not only act on that click ourselves, but also publish an event that others can listen to. We will have to add a JavaScript event listener in the HTML rendered for the Shuffler. This client side code is triggered when the image is clicked. It will submit the form - after it has added an input element to the DOM and set a value on it. Note: this approach to have a custom component trigger an event that can be received by the server side renderer class has been described in Pro JSF and Ajax: Building Rich Internet Components - by John R. Fallows and Jonas Jacobi, the guys who first introduced me to JavaServer Faces. The JavaScript for the Shuffler component looks like this: /** * The onclick handler for ShufflerRenderer. * * @param formClientId the clientId of the enclosing UIForm component * @param clientId the clientId of the Shuffler component */ function _shuffle_click( formClientId, clientId) { var form = document.forms[formClientId]; var input = form[clientId]; if (!input) // if the input element does not already exist, create it and add it to the form { input = document.createElement("input"); input.type = 'hidden'; input.name = clientId; form.appendChild(input); } input.value = 'clicked'; form.submit(); } The JavaScript is not be directly included in the page - as it is part of the jar file in which the Shuffler component is shipped. We need a way to attach this JavaScript (it is in a file called shuffle.js) to the page from within the custom component, or in this case rather its Renderer class. We extend the ResourceServerPhaseListener to also handle JavaScript resources, just like it can handle images. public class ResourceServerPhaseListener implements PhaseListener { public static final String RENDER_SCRIPT_TAG = "/js/"; public static final String RENDER_IMAGE_TAG = "/images/"; public static final String SCRIPT_PATH = "/js/"; public static final String IMAGE_PATH = "/images/"; public PhaseId getPhaseId() { return PhaseId.RESTORE_VIEW; } public void afterPhase(PhaseEvent event) { // If this is restoreView phase if (PhaseId.RESTORE_VIEW == event.getPhaseId()) { // if the request is for a JavaScript library if (-1 != event.getFacesContext().getViewRoot().getViewId().indexOf(RENDER_SCRIPT_TAG)) { // extract the name of the script from the ViewId String script = event.getFacesContext().getViewRoot().getViewId().substring(event.getFacesContext() .getViewRoot().getViewId().indexOf(RENDER_SCRIPT_TAG) + RENDER_SCRIPT_TAG.length()); // render the script writeScript(event, script); event.getFacesContext().responseComplete(); } ... image handling, same as before } } public void beforePhase(PhaseEvent event) { } private void writeScript(PhaseEvent event, String resourceName) { URL url = getClass().getResource(SCRIPT_PATH + resourceName); URLConnection conn = null; InputStream stream = null; BufferedReader bufReader = null; HttpServletResponse response = (HttpServletResponse)event.getFacesContext().getExternalContext().getResponse(); OutputStreamWriter outWriter = null; String curLine = null; try { outWriter = new OutputStreamWriter(response.getOutputStream(), response.getCharacterEncoding()); conn = url.openConnection(); conn.setUseCaches(false); stream = conn.getInputStream(); bufReader = new BufferedReader(new InputStreamReader(stream)); response.setContentType("text/javascript"); response.setStatus(200); while (null != (curLine = bufReader.readLine())) { outWriter.write(curLine + "\n"); } outWriter.close(); } catch (Exception e) { String message = null; message = "Can't load script file:" + url.toExternalForm(); try { response.sendError(HttpServletResponse.SC_BAD_REQUEST, message); } catch (IOException f) { f.printStackTrace(); } } } private void writeImage(PhaseEvent event, String resourceName) { ... same as before } } The Renderer class is responsible for rendering the markup that will include the JavaScript resources to the page (the script element). We could have multiple occurrences of our custom component in a page. However, the JavaScript file shuffle.js should be loaded only once, to prevent excessive and completely pointless browser requests. In order to make that happen, the Renderer indicates to a method writeScriptResource that it has a JavaScript resource that should be included. This method verifies whether a script tag for downloading that same resource has already been added in the current request. If so, it will not add another script tag. If not [already included] then the tag is added with its src attribute referring to the proper PhaseListener controlled url: protected void writeScriptResource( FacesContext context, String resourcePath) throws IOException { Set scriptResources = _getScriptResourcesAlreadyWritten(context); // Set.add() returns true only if item was added to the set // and returns false if item was already present in the set if (scriptResources.add(resourcePath)) { ViewHandler handler = context.getApplication().getViewHandler(); String resourceURL = handler.getResourceURL(context, SCRIPT_PATH +resourcePath); ResponseWriter out = context.getResponseWriter(); out.startElement("script", null); out.writeAttribute("type", "text/javascript", null); out.writeAttribute("src", resourceURL, null); out.endElement("script"); } } private Set _getScriptResourcesAlreadyWritten( FacesContext context) { ExternalContext external = context.getExternalContext(); Map requestScope = external.getRequestMap(); Set written = (Set)requestScope.get(_SCRIPT_RESOURCES_KEY); if (written == null) { written = new HashSet(); requestScope.put(_SCRIPT_RESOURCES_KEY, written); } return written; } static private final String _SCRIPT_RESOURCES_KEY = ShufflerRenderer.class.getName() + ".SCRIPTS_WRITTEN"; With these helper methods in place, the ShufflerRenderer can be extended to include the client side click handling code: @Override public void encodeBegin(final FacesContext facesContext, final UIComponent component) throws IOException { super.encodeBegin(facesContext, component); final Map attributes = component.getAttributes(); final ResponseWriter writer = facesContext.getResponseWriter(); String formClientId = _findFormClientId(facesContext, component); String shuffleClientId = component.getClientId(facesContext); writeScriptResource(context, "shuffle.js"); writer.startElement("DIV", component); String styleClass = (String)attributes.get(Shuffler.STYLECLASS_ATTRIBUTE_KEY); writer.writeAttribute("class", styleClass, null); writer.startElement("SPAN", component); writer.writeAttribute("onClick", "_shuffle_click('" + formClientId + "'," + "'" + shuffleClientId + "')", null); writer.startElement("IMG", component); writer.writeAttribute("src", imageUrl( facesContext,SHUFFLE_IMAGE), null); writer.writeAttribute("alt", "Click to reshuffle", null); writer.writeAttribute("width", "20px", null); writer.endElement("IMG"); writer.endElement("SPAN"); List orderedFacets = ((Shuffler)component).getOrderedFacets(facesContext); for (UIComponent facet:orderedFacets) { facet.encodeAll(facesContext); } } protected void encodeResources(FacesContext context, UIComponent component) throws IOException { writeScriptResource(context, "shuffle.js"); } /** * Finds the parent UIForm component client identifier. * * @param context the Faces context * @param component the Faces component * * @return the parent UIForm or RichForm (for usage in ADF) client identifier, if present, otherwise null */ private String _findFormClientId(FacesContext context, UIComponent component) { if (component==null) { return null; } if (component instanceof UIForm || component.getClass().getName().endsWith("RichForm")) { return component.getClientId(context); } else { return _findFormClientId(context, component.getParent()); } } The image is wrapped in a SPAN and the onclick event handler is defined on that SPAN element (this allows us to later on add more clickable stuff to the SPAN). When the image is clicked, the _shuffle_click function is invoked - that was loaded from shuffle.js. The element is added to the form and the form is submitted. The HTML rendered from this renderer now looks like this:
October 7, 2009
by Wouter Van Reeven
·
43,745 Views
·
0 Likes