web-dev-qa-db-fra.com

Comment créer des champs de formulaire JSF dynamiques

J'ai trouvé des questions similaires comme this one, mais il y a tellement de façons de faire que cela m'a rendu plus confus.

Nous obtenons un fichier XML que nous lisons. Ce XML contient des informations sur certains champs de formulaire qui doivent être présentés.

J'ai donc créé ce DynamicField.Java Personnalisé qui contient toutes les informations dont nous avons besoin:

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

Nous avons donc un List<DynamicField>.

Je veux parcourir cette liste et remplir les champs du formulaire pour qu'il ressemble à ceci:

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

Le <my:someCustomComponent> Renverrait alors les composants de formulaire JSF appropriés (c.-à-d. Label, inputText)

Une autre approche serait d'afficher simplement le <my:someCustomComponent> Et cela retournerait un HtmlDataTable avec des éléments de formulaire. (Je pense que c'est peut-être plus facile à faire).

Quelle approche est la meilleure? Quelqu'un peut-il me montrer des liens ou du code où il montre comment je peux créer cela? Je préfère des exemples de code complets, et non des réponses comme "Vous avez besoin d'une sous-classe de javax.faces.component.UIComponent".

21
Shervin Asgari

Puisque l'Origin n'est en fait pas XML, mais un Javabean, et l'autre réponse ne mérite pas d'être éditée dans une saveur totalement différente (elle peut toujours être utile pour de futures références par d'autres), j'ajouterai réponse basée sur une origine Javabean.


Je vois essentiellement trois options lorsque l'Origin est un Javabean.

  1. Utilisez l'attribut JSF rendered ou même les balises JSTL <c:choose>/<c:if> Pour effectuer le rendu ou la construction conditionnelle du ou des composants souhaités. Voici un exemple utilisant l'attribut rendered:

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

    Un exemple d'approche JSTL peut être trouvé à Comment faire une grille de composant composite JSF? Non, JSTL n'est absolument pas une "mauvaise pratique". Ce mythe est un vestige de l'ère JSF 1.x et persiste trop longtemps car les débutants ne comprenaient pas clairement le cycle de vie et les pouvoirs de JSTL. Au point, vous pouvez utiliser JSTL uniquement lorsque le modèle derrière #{bean.fields} Comme dans l'extrait ci-dessus ne change jamais pendant au moins la portée de la vue JSF. Voir aussi JSTL dans JSF2 Facelets ... est-il logique? À la place, utiliser binding pour une propriété de bean est toujours une "mauvaise pratique".

    Quant au <ui:repeat><div>, Peu importe le composant d'itération que vous utilisez, vous pouvez même utiliser <h:dataTable> Comme dans votre question initiale, ou un composant d'itération spécifique à la bibliothèque de composants, tel que <p:dataGrid> Ou <p:dataList>. Refactorisez si nécessaire le gros morceau de code en un fichier inclus ou tag .

    Quant à la collecte des valeurs soumises, le #{bean.values} Doit pointer vers un Map<String, Object> Qui est déjà précréé. Un HashMap suffit. Vous voudrez peut-être préremplir la carte dans le cas de contrôles qui peuvent définir plusieurs valeurs. Vous devez ensuite le préremplir avec un List<Object> Comme valeur. Notez que je m'attends à ce que la Field#getType() soit un enum car cela facilite le traitement du côté code Java. Vous pouvez ensuite utiliser un switch instruction au lieu d'un mauvais bloc if/else.


  2. Créez les composants par programmation dans un écouteur d'événement postAddToView:

    <h:form id="form">
        <f:event type="postAddToView" listener="#{bean.populateForm}" />
    </h:form>
    

    Avec:

    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: ne créez PAS vous-même le HtmlForm! utilisez celui créé par JSF, celui-ci n'est jamais null)

    Cela garantit que l'arborescence est remplie exactement au bon moment, et garde les getters libres de toute logique métier, et évite les problèmes potentiels de "doublon d'ID de composant" lorsque #{bean} Est dans une portée plus large que la portée de la demande (vous pouvez donc utilisez en toute sécurité, par exemple, un bean de portée de vue ici) et conserve le bean exempt de propriétés UIComponent, ce qui évite à son tour des problèmes potentiels de sérialisation et des fuites de mémoire lorsque le composant est détenu comme propriété d'un bean sérialisable.

    Si vous êtes toujours sur JSF 1.x où <f:event> N'est pas disponible, liez plutôt le composant de formulaire à un bean à portée de requête (pas de session!) Via binding

    <h:form id="form" binding="#{bean.form}" />
    

    Et puis remplissez-le paresseusement dans le getter du formulaire:

    public HtmlForm getForm() {
        if (form == null) {
            form = new HtmlForm();
            // ... (continue with code as above)
        }
        return form;
    }
    

    Lors de l'utilisation de binding, il est très important de comprendre que les composants d'interface utilisateur sont essentiellement limités à la demande et ne doivent absolument pas être affectés en tant que propriété d'un bean dans une portée plus large. Voir aussi Comment fonctionne l'attribut 'binding' dans JSF? Quand et comment doit-il être utilisé?


  3. Créez un composant personnalisé avec un rendu personnalisé. Je ne vais pas publier d'exemples complets car c'est beaucoup de code qui serait après tout un gâchis très serré et spécifique à l'application.


Les avantages et les inconvénients de chaque option doivent être clairs. Il va du plus facile et du mieux maintenable au plus dur et le moins maintenable et par la suite aussi du moins réutilisable au meilleur réutilisable. C'est à vous de choisir ce qui convient le mieux à vos besoins fonctionnels et à votre situation actuelle.

Il convient de noter qu'il n'y a absolument rien qui est seulement possible dans Java (voie # 2) et impossible en XHTML + XML (voie # 1). Tout est possible en XHTML + XML aussi bien qu'en Java. Beaucoup de démarreurs sous-estiment XHTML + XML (en particulier <ui:repeat> et JSTL) dans créer dynamiquement des composants et penser à tort que Java serait le "seul et unique" moyen, alors que cela ne se termine généralement que par du code fragile et déroutant.

55
BalusC

Si l'origine est XML, je suggère d'opter pour une approche complètement différente: XSL . Facelets est basé sur XHTML. Vous pouvez facilement utiliser XSL pour passer de XML à XHTML. C'est faisable avec un peu décent Filter qui intervient avant que JSF ne fasse les travaux.

Voici un exemple de lancement.

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 qui est mappé sur <servlet-name> du FacesServlet et suppose que le FacesServlet lui-même est mappé sur un <url-pattern> de *.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);
}

Géré par http://example.com/context/persons.jsf et ce filtre se déclenchera et transformera persons.xml à persons.xhtml en utilisant persons.xsl et enfin mettre persons.xhtml là où JSF l'attend.

Certes, XSL a un peu de courbe d'apprentissage, mais c'est l'OMI le bon outil pour le travail puisque la source est XML et la destination est basée sur XML comme wel.

Pour effectuer le mappage entre le formulaire et le bean géré, utilisez simplement un Map<String, Object>. Si vous nommez les champs de saisie ainsi

<h:inputText value="#{bean.map.field1}" />
<h:inputText value="#{bean.map.field2}" />
<h:inputText value="#{bean.map.field3}" />
...

Les valeurs soumises seront disponibles par Map touches field1, field2, field3, etc.

17
BalusC