How to create dynamic JSF form fields
Solution 1:
Since the origin is actually not XML, but a Javabean, and the other answer doesn't deserve to be edited into a totally different flavor (it may still be useful for future references by others), I'll add another answer based on a Javabean-origin.
I see basically three options when the origin is a Javabean.
-
Make use of JSF
rendered
attribute or even JSTL<c:choose>
/<c:if>
tags to conditionally render or build the desired component(s). Below is an example usingrendered
attribute:<ui:repeat value="#{bean.fields}" var="field"> <div class="field"> <h:inputText value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXT'}" /> <h:inputSecret value="#{bean.values[field.name]}" rendered="#{field.type == 'SECRET'}" /> <h:inputTextarea value="#{bean.values[field.name]}" rendered="#{field.type == 'TEXTAREA'}" /> <h:selectOneRadio value="#{bean.values[field.name]}" rendered="#{field.type == 'RADIO'}"> <f:selectItems value="#{field.options}" /> </h:selectOneRadio> <h:selectOneMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTONE'}"> <f:selectItems value="#{field.options}" /> </h:selectOneMenu> <h:selectManyMenu value="#{bean.values[field.name]}" rendered="#{field.type == 'SELECTMANY'}"> <f:selectItems value="#{field.options}" /> </h:selectManyMenu> <h:selectBooleanCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKONE'}" /> <h:selectManyCheckbox value="#{bean.values[field.name]}" rendered="#{field.type == 'CHECKMANY'}"> <f:selectItems value="#{field.options}" /> </h:selectManyCheckbox> </div> </ui:repeat>
An example of JSTL approach can be found at How to make a grid of JSF composite component? No, JSTL is absolutely not a "bad practice". This myth is a leftover from JSF 1.x era and continues too long because starters didn't clearly understand the lifecycle and powers of JSTL. To the point, you can use JSTL only when the model behind
#{bean.fields}
as in above snippet does not ever change during at least the JSF view scope. See also JSTL in JSF2 Facelets... makes sense? Instead, usingbinding
to a bean property is still a "bad practice".As to the
<ui:repeat><div>
, it really doesn't matter which iterating component you use, you can even use<h:dataTable>
as in your initial question, or a component library specific iterating component, such as<p:dataGrid>
or<p:dataList>
. Refactor if necessary the big chunk of code to an include or tagfile.As to collecting the submitted values, the
#{bean.values}
should point to aMap<String, Object>
which is already precreated. AHashMap
suffices. You may want to prepopulate the map in case of controls which can set multiple values. You should then prepopulate it with aList<Object>
as value. Note that I expect theField#getType()
to be anenum
since that eases the processing in the Java code side. You can then use aswitch
statement instead of a nastyif/else
block.
-
Create the components programmatically in a
postAddToView
event listener:<h:form id="form"> <f:event type="postAddToView" listener="#{bean.populateForm}" /> </h:form>
With:
public void populateForm(ComponentSystemEvent event) { HtmlForm form = (HtmlForm) event.getComponent(); for (Field field : fields) { switch (field.getType()) { // It's easiest if it's an enum. case TEXT: UIInput input = new HtmlInputText(); input.setId(field.getName()); // Must be unique! input.setValueExpression("value", createValueExpression("#{bean.values['" + field.getName() + "']}", String.class)); form.getChildren().add(input); break; case SECRET: UIInput input = new HtmlInputSecret(); // etc... } } }
(note: do NOT create the
HtmlForm
yourself! use the JSF-created one, this one is nevernull
)This guarantees that the tree is populated at exactly the right moment, and keeps getters free of business logic, and avoids potential "duplicate component ID" trouble when
#{bean}
is in a broader scope than the request scope (so you can safely use e.g. a view scoped bean here), and keeps the bean free ofUIComponent
properties which in turn avoids potential serialization trouble and memory leaking when the component is held as a property of a serializable bean.If you're still on JSF 1.x where
<f:event>
is not available, instead bind the form component to a request (not session!) scoped bean viabinding
<h:form id="form" binding="#{bean.form}" />
And then lazily populate it in the getter of the form:
public HtmlForm getForm() { if (form == null) { form = new HtmlForm(); // ... (continue with code as above) } return form; }
When using
binding
, it's very important to understand that UI components are basically request scoped and should absolutely not be assigned as a property of a bean in a broader scope. See also How does the 'binding' attribute work in JSF? When and how should it be used?
-
Create a custom component with a custom renderer. I am not going to post complete examples since that's a lot of code which would after all be a very tight-coupled and application-specific mess.
Pros and cons of each option should be clear. It goes from most easy and best maintainable to most hard and least maintainable and subsequently also from least reuseable to best reuseable. It's up to you to pick whatever the best suits your functional requirement and current situation.
Noted should be that there is absolutely nothing which is only possible in Java (way #2) and impossible in XHTML+XML (way #1). Everything is possible in XHTML+XML as good as in Java. A lot of starters underestimate XHTML+XML (particularly <ui:repeat>
and JSTL) in dynamically creating components and incorrectly think that Java would be the "one and only" way, while that generally only ends up in brittle and confusing code.
Solution 2:
If the origin is XML, I suggest to go for a completely different approach: XSL. Facelets is XHTML based. You can easily use XSL to go from XML to XHTML. This is doable with a bit decent Filter
which kicks in before JSF is doing the works.
Here's a kickoff example.
persons.xml
<?xml version="1.0" encoding="UTF-8"?>
<persons>
<person>
<name>one</name>
<age>1</age>
</person>
<person>
<name>two</name>
<age>2</age>
</person>
<person>
<name>three</name>
<age>3</age>
</person>
</persons>
persons.xsl
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet
xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="2.0"
xmlns:f="http://java.sun.com/jsf/core"
xmlns:h="http://java.sun.com/jsf/html">
<xsl:output method="xml"
doctype-public="-//W3C//DTD XHTML 1.0 Transitional//EN"
doctype-system="http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/>
<xsl:template match="persons">
<html>
<f:view>
<head><title>Persons</title></head>
<body>
<h:panelGrid columns="2">
<xsl:for-each select="person">
<xsl:variable name="name"><xsl:value-of select="name" /></xsl:variable>
<xsl:variable name="age"><xsl:value-of select="age" /></xsl:variable>
<h:outputText value="{$name}" />
<h:outputText value="{$age}" />
</xsl:for-each>
</h:panelGrid>
</body>
</f:view>
</html>
</xsl:template>
</xsl:stylesheet>
JsfXmlFilter
which is mapped on <servlet-name>
of the FacesServlet
and assumes that the FacesServlet
itself is mapped on an <url-pattern>
of *.jsf
.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
HttpServletRequest r = (HttpServletRequest) request;
String rootPath = r.getSession().getServletContext().getRealPath("/");
String uri = r.getRequestURI();
String xhtmlFileName = uri.substring(uri.lastIndexOf("/")).replaceAll("jsf$", "xhtml"); // Change this if FacesServlet is not mapped on `*.jsf`.
File xhtmlFile = new File(rootPath, xhtmlFileName);
if (!xhtmlFile.exists()) { // Do your caching job.
String xmlFileName = xhtmlFileName.replaceAll("xhtml$", "xml");
String xslFileName = xhtmlFileName.replaceAll("xhtml$", "xsl");
File xmlFile = new File(rootPath, xmlFileName);
File xslFile = new File(rootPath, xslFileName);
Source xmlSource = new StreamSource(xmlFile);
Source xslSource = new StreamSource(xslFile);
Result xhtmlResult = new StreamResult(xhtmlFile);
try {
Transformer transformer = TransformerFactory.newInstance().newTransformer(xslSource);
transformer.transform(xmlSource, xhtmlResult);
} catch (TransformerException e) {
throw new RuntimeException("Transforming failed.", e);
}
}
chain.doFilter(request, response);
}
Run by http://example.com/context/persons.jsf and this filter will kick in and transform persons.xml
to persons.xhtml
using persons.xsl
and finally put persons.xhtml
there where JSF expect it is.
True, XSL has a bit of learning curve, but it's IMO the right tool for the job since the source is XML and destination is XML based as wel.
To do the mapping between the form and the managed bean, just use a Map<String, Object>
. If you name the input fields like so
<h:inputText value="#{bean.map.field1}" />
<h:inputText value="#{bean.map.field2}" />
<h:inputText value="#{bean.map.field3}" />
...
The submitted values will be available by Map
keys field1
, field2
, field3
, etc.