In my last post I showed you the composite component collapsiblePanel. In my final remark I mentioned that this component has a major design flaw. The logic for collapsing and expanding the content must be supplied by the user and is not part of the component. In this post I show you how this problem can be solved.
Every now and then parts of a composite component’s intended behaviour can only be implemented with Java code. Just like our logic for collapsing and expanding the panel. What we need is an extension point to integrate this code into a composite component. As composite components are internally built with a set of classical components the most obvious idea is to use a component for this purpose.
And that is exactly what JSF does. By setting the attribute componentType of cc:interface we can tell JSF to use a custom root compont for the composite component. For our example this means we create a component with a collapsed property and a toggle method. The following listing shows the code.
@FacesComponent("at.jsflive.CollapsiblePanel")
public class CollapsiblePanel extends UINamingContainer {
enum PropertyKeys {collapsed}
public boolean isCollapsed() {
return (Boolean)getStateHelper().eval(
PropertyKeys.collapsed, Boolean.FALSE);
}
public void setCollapsed(boolean collapsed) {
getStateHelper().put(PropertyKeys.collapsed, collapsed);
}
public void toggle(ActionEvent e) {
setCollapsed(!isCollapsed());
}
}
As the root component of a composite component must be a naming container the class extends UINamingContainer.
The following listing shows the updated composite component. The changes compared to the version from the last post are small and mainly affect the attributes and their wiring to the internal components.
<cc:interface componentType="at.jsflive.CollapsiblePanel">
<cc:attribute name="collapsed"/>
<cc:actionSource name="toggle"/>
<cc:facet name="header"/>
</cc:interface>
<cc:implementation>
<h:panelGroup layout="block" styleClass="collapsiblePanel-header">
<h:commandButton id="toggle" actionListener="#{cc.toggle}"
styleClass="collapsiblePanel-img"
image="#{resource[cc.collapsed
? 'jsflive:plus.png' : 'jsflive:minus.png']}"/>
<cc:renderFacet name="header"/>
</h:panelGroup>
<h:panelGroup layout="block" rendered="#{!cc.collapsed}">
<cc:insertChildren/>
</h:panelGroup>
<h:outputStylesheet library="jsflive" name="components.css"/>
</cc:implementation>
First, the required model attribute is replaced with the optional collapsed attribute. This one is used to set the initial collapse state from the outside. Its value is automatically considered by the StateHelper in isCollapsed().
The second change affects the method toggle and the attribute collapsed. As both of them are now provided by the root component the EL expressions for accessing them must be changed to cc.toggle and cc.collapsed. This is possible because cc now points to a component of class CollapsiblePanel. Using cc.attrs is no longer necessary.
The following listing shows the usage of the new component in a page. In comparison the last post the toggle method of the managed bean is no longer needed.
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:jl="http://java.sun.com/jsf/composite/jsflive">
<h:head>
<title>JSFlive test page</title>
</h:head>
<h:body>
<h:form id="form">
<h:outputText value="JSFlive collapsible panel test page"/>
<jl:collapsiblePanel id="panel"
collapsed="#{testController.collapsed}">
<f:facet name="header">
<h3><h:outputText value="Collapsible information"/></h3>
</f:facet>
<h:outputText value="This information is collapsible."/>
</jl:collapsiblePanel>
</h:form>
</h:body>
</html>
With the changes presented in this post the composite component collapsiblePanel is now really self-contained and reusable.
One last note: managed bean properties referenced in collapsed are currently not updated if the state changes. This can be done by calling the following method in CollapsiblePanel.toggle():
private void setCollapsedValueExpression() {
ELContext ctx = FacesContext.getCurrentInstance().getELContext();
ValueExpression ve = getValueExpression(PropertyKeys.collapsed.name());
if (ve != null) {
ve.setValue(ctx, isCollapsed());
}
}
The source code for the presented example can be found in the Github repository collapsible02.
Pingback: Ajaxify composite component collapsiblePanel | JSFlive: Michael Kurz's JSF Weblog