How to create dynamic JSF form fields

…衆ロ難τιáo~ 提交于 2019-12-27 08:18:29

问题


I have found some similar questions like this one, however there are so many ways this can be done that it made me more confused.

We are getting an XML file that we are reading. This XML contains information on some form fields that needs to be presented.

So I created this custom DynamicField.java that has all the information we need:

public class DynamicField {
  private String label; // label of the field
  private String fieldKey; // some key to identify the field
  private String fieldValue; // the value of field
  private String type; // can be input,radio,selectbox etc

  // Getters + setters.
}

So we have a List<DynamicField>.

I want to iterate through this list and populate the form fields so it looks something like this:

<h:dataTable value="#{dynamicFields}" var="field">
    <my:someCustomComponent value="#{field}" />
</h:dataTable>

The <my:someCustomComponent> would then return the appropriate JSF form components (i.e. label, inputText)

Another approach would be to just display the <my:someCustomComponent> and then that would return an HtmlDataTable with form elements. (I think this is maybe easier to do).

Which approach is best? Can someone show me to some links or code where it shows how I can create this? I prefer complete code examples, and not answers like "You need a subclass of javax.faces.component.UIComponent".


回答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.

  1. 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 using rendered 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, using binding 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 a Map<String, Object> which is already precreated. A HashMap suffices. You may want to prepopulate the map in case of controls which can set multiple values. You should then prepopulate it with a List<Object> as value. Note that I expect the Field#getType() to be an enum since that eases the processing in the Java code side. You can then use a switch statement instead of a nasty if/else block.


  2. 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 never null)

    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 of UIComponent 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 via binding

    <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?


  3. 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.




回答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.



来源:https://stackoverflow.com/questions/3510614/how-to-create-dynamic-jsf-form-fields

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!