Tutoriel sur la création d'un composant JSF CP/Ville

Puzzle java JSF Richfaces

Cet article présente une méthodologie pour créer un composant JSF de deux manières différentes.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum. Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Site personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Nous allons aborder les concepts de réutilisabilité en JSF en créant un composant CPCode Postal/Ville avec Richfaces 4.5 de deux manières différentes :

  1. Via le principe d'include de facelet ;
  2. En créant un composant composite, plus connu sous son terme anglais composite component.

II. Présentation du composant

Le composant CP/Ville permet de trouver une ville en tapant son code postal. Si plusieurs villes correspondent à la saisie du code postal, une liste déroulante permet de choisir la ville. Dans le cas contraire, le champ ville est automatiquement rempli avec l'unique ville correspondante. La vidéo ci-après montre son utilisation :

Au niveau du modèle de données :

  • une ville ne peut avoir qu'un code postal cependant un code postal peut-être associé à plusieurs villes ;
  • la correspondance entre les villes et les codes postaux se fait via une table code_postal dont voici la structure
Image non disponible

Voici un échantillon de données :

Image non disponible

Dans le code source du fieldset « Adresse Pro », le futur composant est le panelGroup #pro et sa représentation selon le modèle des boîtes est la suivante :

Image non disponible
 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
<fieldset style="max-width:600px">
    <legend>Adresse Pro</legend>
    <h:panelGrid columns="2" >

        <h:outputLabel value="Adresse " for="pro_adresse" />
        <h:inputText id="pro_adresse" value="#{exempleCpVilleBean.proAdresse}" 
            style="width: 262px"
            onchange="notifyChange()" 
            tabindex="1" />    

        <h:panelGroup>
            <h:outputLabel value="CP " for="pro_cp" />/
            <h:outputLabel value=" Ville" for="pro_ville" />
        </h:panelGroup>

        <h:panelGroup id="pro" layout="block" styleClass="middle">
            <a4j:jsFunction id="fCp_pro" name="searchCp_pro" immediate="true" render="pro_pgVille" limitRender="true"
                oncomplete="keeUtils.goToNextTabIndex( #{rich:element('pro_cp')} );">
                <a4j:param name="param1" assignTo="#{exempleCpVilleBean.proCp}" />
                <f:setPropertyActionListener value="#{exempleCpVilleBean.proCp}" target="#{cpVilleBean.cp}" />
                <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{exempleCpVilleBean.proVille}" />
            </a4j:jsFunction>

            <h:panelGroup layout="block" style="display:inline-block" styleClass="middle">
                <h:inputText id="pro_cp" value="#{exempleCpVilleBean.proCp}" 
                    maxlength="5" 
                    style="width:55px;margin:0px;" 
                    styleClass="smoothReadonly middle"
                    required="true" label="Cp Pro"
                    onkeyup="if ($(event).which != 16) { if (keeUtils.cpOK(this.value)) { searchCp_pro(this.value); }}"
                    onchange="notifyChange()" 
                    tabindex="2" >
                </h:inputText>
            </h:panelGroup>
            <h:panelGroup layout="block" id="pro_pgVille" style="display:inline-block;margin:0px 0px 0px 3px" styleClass="middle" >
                    <h:inputText id="pro_ville" value="#{exempleCpVilleBean.proVille}" 
                        style="width:200px;margin:0px;" 
                        styleClass="smoothReadonly"
                        rendered="#{not empty exempleCpVilleBean.proVille or empty cpVilleBean.villes or empty exempleCpVilleBean.proCp}"
                        onchange="notifyChange()" 
                        required="true" label="Ville pro"
                        tabindex="3" />

                    <rich:autocomplete id="pro_villes" value="#{exempleCpVilleBean.proVille}" 
                        inputClass="pro_comboVille"
                        styleClass="middle"
                        rendered="#{not empty cpVilleBean.villes and empty exempleCpVilleBean.proVille and (fn:length(exempleCpVilleBean.proCp)==5)}" 
                        onchange="notifyChange()"
                        required="true" label="Ville pro"
                        tabindex="3" 
                        mode="client" showButton="true" selectFirst="true" autofill="true" layout="div" 
                        autocompleteList="#{cpVilleBean.villes}" var="vil" fetchValue="#{vil.value}" >
                        #{vil.label}
                    </rich:autocomplete>
            </h:panelGroup>
        </h:panelGroup>
    </h:panelGrid>
</fieldset>

Côté Java, je ne vais pas entrer dans les détails. J'utilise le bean managé CpVilleBean qui s'occupe de rechercher la/les ville/s associée/s au CP saisi et qui construit une liste de selectItem pour alimenter le combo dans le cas où il y a plusieurs villes.

Extrait de CpVilleBean.java
CacherSélectionnez

Maintenant que je vous ai présenté les fonctionnalités du composant et sa structure, le but est de factoriser ce code afin de pouvoir le réutiliser simplement sans avoir à faire un copier/coller.

III. Préparation

Avant factorisation, le code de la vidéo d'exemple ressemble à ceci :

Avant factorisation
CacherSélectionnez

Après factorisation, suivant les deux méthodes que je vais vous présenter, nous obtiendrons :

Après factorisation
CacherSélectionnez

Voici un comparatif visuel avant/après :

Image non disponible
Bloc adresse pro
Image non disponible
Bloc adresse perso

Certains éléments vont devenir des paramètres suite à la factorisation du code de notre futur composant. Ainsi la première étape dans la création d'un composant consiste à identifier ses paramètres. Comment ? En faisant exactement ce que nous voulons éviter de faire : un copier/coller. Nous allons donc :

  1. Dupliquer les éléments du composant dans la même page via un copier/coller du code ;
  2. Le rendre fonctionnel en modifiant un minimum de code ;
  3. Comparer les éléments qui diffèrent ;
  4. Identifier le code spécifique au scénario de la page courante.

Après avoir testé que les deux composants fonctionnent bien dans une même page, je les ai isolés dans deux fichiers différents et les ai comparés (en utilisant la fonction « compare with each other » d'Eclipse par exemple).

recherche des différences

Les éléments qui diffèrent sont donc :

  • la propriété cp du bean, valeur du champ de saisie du cp ;
  • la propriété ville du bean, valeur du champ de saisie de ville ;
  • les id ;
  • le nom de la fonction de recherche par CP ;
  • les labels des champs de saisie ;
  • les tabindex ;
  • la taille de l'input text ville ;
  • la classe du combo ville (qui permet d'agir sur sa taille) ;
  • la fonction notifyChange dans l'événement onchange : elle permet de dégriser le bouton « Enregistrer » suite à une modification de la valeur du champ, elle est donc spécifique au scénario de la page courante.

Nous pouvons passer à l'étape suivante : la création du composant selon la 1re méthode.

IV. Méthode 1 : Composant facelet par inclusion

La première méthode de création du composant consiste à externaliser le code dans une facelet, un fichier à part que je nomme cpVille.xhtml et que je place de manière arbitraire dans le dossier WebContent/composant/

/WebContent/composant/cpVille.xhtml
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:f="http://java.sun.com/jsf/core"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:fn="http://java.sun.com/jsp/jstl/functions"
      xmlns:rich="http://richfaces.org/rich"
      xmlns:a4j="http://richfaces.org/a4j" > 

<ui:composition>
    <h:panelGroup id="pro" layout="block" styleClass="middle">
        <a4j:jsFunction id="fCp_pro" name="searchCp_pro" immediate="true" render="pro_pgVille" limitRender="true"
            oncomplete="keeUtils.goToNextTabIndex( #{rich:element('pro_cp')} );">
            <a4j:param name="param1" assignTo="#{exempleCpVilleBean.proCp}" />
            <f:setPropertyActionListener value="#{exempleCpVilleBean.proCp}" target="#{cpVilleBean.cp}" />
            <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{exempleCpVilleBean.proVille}" />
        </a4j:jsFunction>

        <h:panelGroup layout="block" style="display:inline-block" styleClass="middle">
            <h:inputText id="pro_cp" value="#{exempleCpVilleBean.proCp}" 
                maxlength="5" 
                style="width:55px;margin:0px;" 
                styleClass="smoothReadonly middle"
                required="true" label="Cp Pro"
                onkeyup="if ($(event).which != 16) { if (keeUtils.cpOK(this.value)) { searchCp_pro(this.value); }}"
                onchange="notifyChange()" 
                tabindex="2" >
            </h:inputText>
        </h:panelGroup>
        <h:panelGroup layout="block" id="pro_pgVille" style="display:inline-block;margin:0px 0px 0px 3px" styleClass="middle" >
                <h:inputText id="pro_ville" value="#{exempleCpVilleBean.proVille}" 
                    style="width:200px;margin:0px;" 
                    styleClass="smoothReadonly"
                    rendered="#{not empty exempleCpVilleBean.proVille or empty cpVilleBean.villes or empty exempleCpVilleBean.proCp}"
                    onchange="notifyChange()" 
                    required="true" label="Ville pro"
                    tabindex="3" />

                <rich:autocomplete id="pro_villes" value="#{exempleCpVilleBean.proVille}" 
                    inputClass="pro_comboVille"
                    styleClass="middle"
                    rendered="#{not empty cpVilleBean.villes and empty exempleCpVilleBean.proVille and (fn:length(exempleCpVilleBean.proCp)==5)}" 
                    onchange="notifyChange()"
                    required="true" label="Ville pro"
                    tabindex="3" 
                    mode="client" showButton="true" selectFirst="true" autofill="true" layout="div" 
                    autocompleteList="#{cpVilleBean.villes}" var="vil" fetchValue="#{vil.value}" >
                    #{vil.label}
                </rich:autocomplete>
        </h:panelGroup>
    </h:panelGroup>
</ui:composition>
</html>

La balise la plus importante est <ui:composition> qui encapsule le code du composant. Attention, tout ce qui est à l'extérieur de cette balise ne sera pas pris en compte !

Dès maintenant, <ui:include> me permet de remplacer les champs cp et ville dans mon formulaire exemple de la manière suivante :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
<fieldset style="max-width:600px">
    <legend>Adresse Pro</legend>
    <h:panelGrid columns="2" >

        <h:outputLabel value="Adresse " for="pro_adresse" />
        <h:inputText id="pro_adresse" value="#{exempleCpVilleBean.proAdresse}" 
            style="width: 262px"/>    

        <h:panelGroup>
            <h:outputLabel value="CP " for="pro_cp" />/
            <h:outputLabel value=" Ville" for="pro_ville" />
        </h:panelGroup>

        <ui:include src="/composant/cpVille.xhtml" ></ui:include>
    </h:panelGrid>
</fieldset>

Ensuite, je n'ai plus qu'à placer mes paramètres en utilisant des « Expression Languages » #{nom_param} ce qui donne :

/WebContent/composant/cpVille.xhtml
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
<ui:composition>
    <h:panelGroup id="#{id}" layout="block" styleClass="middle">

        <ui:param name="idCp" value="#{id}_cp" />
        <ui:param name="idVille" value="#{id}_ville" />
        <ui:param name="idVilles" value="#{id}_villes" />
        <ui:param name="idGroupVille" value="#{id}_pgVille" />

        <a4j:jsFunction id="fCp_#{id}" name="searchCp_#{id}" immediate="true" render="#{idGroupVille}" limitRender="true"
            oncomplete="keeUtils.goToNextTabIndex( #{rich:element(idCp)} );">
            <a4j:param name="param1" assignTo="#{cp}" />
            <f:setPropertyActionListener value="#{cp}" target="#{cpVilleBean.cp}" />
            <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{ville}" />
        </a4j:jsFunction>

        <h:panelGroup layout="block" style="display:inline-block" styleClass="middle">
            <h:inputText id="#{idCp}" value="#{cp}" 
                maxlength="5" 
                style="width:55px;margin:0px;" 
                styleClass="smoothReadonly middle"
                required="true" label="Cp #{label}"
                onkeyup="if ($(event).which != 16) { if (keeUtils.cpOK(this.value)) { searchCp_#{id}(this.value); }}"
                onchange="#{onchange}" 
                tabindex="#{tabindex}" >
            </h:inputText>
        </h:panelGroup>
        <h:panelGroup layout="block" id="#{idGroupVille}" style="display:inline-block;margin:0px 0px 0px 3px" styleClass="middle" >
                <h:inputText id="#{idVille}" value="#{ville}" 
                    style="width:#{villeWidth};margin:0px;" 
                    styleClass="smoothReadonly"
                    rendered="#{not empty ville or empty cpVilleBean.villes or empty cp}"
                    onchange="#{onchange}" 
                    required="true" label="Ville pro"
                    tabindex="#{(tabindex==null)?'':(tabindex+1)}" />

                <rich:autocomplete id="#{idVilles}" value="#{ville}" 
                    inputClass="#{styleClass}_comboVille"
                    styleClass="middle"
                    rendered="#{not empty cpVilleBean.villes and empty ville and (fn:length(cp)==5)}" 
                    onchange="#{onchange}"
                    required="true" label="Ville #{label}"
                    tabindex="#{(tabindex==null)?'':(tabindex+1)}" 
                    mode="client" showButton="true" selectFirst="true" autofill="true" layout="div" 
                    autocompleteList="#{cpVilleBean.villes}" var="vil" fetchValue="#{vil.value}" >
                    #{vil.label}
                </rich:autocomplete>
        </h:panelGroup>
    </h:panelGroup>
</ui:composition>

Voici la liste des paramètres créés :

  • cp : la valeur du champ de saisie du cp ;
  • ville : la valeur du champ de saisie de la ville ;
  • id : au lieu de créer un paramètre id pour chaque champ, j'utilise un seul paramètre qui sert à construire les autres id en déclarant une variable dans la facelet (<ui:param>). Je peux réutiliser du coup ses variables dans les attributs execute, render, oncomplete de ma fonction. id sert aussi à construire le nom de la fonction JavaScript qui doit être unique pour chaque composant ;
  • label : ici aussi j'utilise un seul paramètre qui servira à construire le label du champ cp et du champ ville (affiché en cas de champ vide) ;
  • tabindex : le tabindex du champ cp ;
  • villeWidth : permet de spécifier la longueur de l'input text ville
  • styleClass : permet de spécifier une classe qui s'appliquera au composant combo ;
  • onchange : fonction JavaScript qui sera exécutée lors du déclenchement de l'événement onchange des champs de saisie.

Pour les passer à ma facelet, il faut utiliser <ui:param> au niveau de l'inclusion :

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
<ui:include src="/composant/cpVille.xhtml">
    <ui:param name="id" value="pro" />
    <ui:param name="cp" value="#{exempleCpVilleBean.proCp}" />
    <ui:param name="ville" value="#{exempleCpVilleBean.proVille}" />
    <ui:param name="villeWidth" value="200px" />
    <ui:param name="styleClass" value="pro" />
    <ui:param name="onchange" value="notifyChange()" />
</ui:include>

Notre composant possède déjà l'essentiel pour être réutilisé dans d'autres écrans, mais il ne permet pas de réaliser ce que la plupart des composants JSF de base permettent à savoir :

  • paramétrer sa visibilité : rendered ;
  • changer son état actif/inactif/lecture seule : disabled / readonly ;
  • rendre un champ obligatoire ou non : required.

De plus, comment faire pour ajouter un événement onkeyup/onblur/onfocus sur l'un des inputs text ?

Vous l'avez compris, nous allons lui ajouter d'autres paramètres.

/WebContent/composant/cpVille.xhtml
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
<ui:composition>

    <ui:param name="rendered" value="#{(rendered==null)?true:rendered}" />

    <h:panelGroup id="#{id}" layout="block" rendered="#{rendered}" styleClass="clearFloat middle">

        <h:outputStylesheet library="default" name="css/cpVille.css" />
        <h:outputScript library="default" name="js/utils.js" target="head" />

        <ui:param name="idCp" value="#{id}_cp" />
        <ui:param name="idVille" value="#{id}_ville" />
        <ui:param name="idVilles" value="#{id}_villes" />
        <ui:param name="idGroupVille" value="#{id}_pgVille" />
        <ui:param name="label" value="#{(label==null)?'':label}" />
        <ui:param name="tabindexSup" value="#{(tabindex==null) ? '' : tabindex + 1}" />
        <ui:param name="readonly" value="#{(readonly==null)?false:readonly}" />
        <ui:param name="disabled" value="#{(disabled==null)?false:disabled}" />
        <ui:param name="required" value="#{(required==null)?false:required}" />
        <ui:param name="villeWidth" value="#{(villeWidth==null)?'170px':villeWidth}" />

        <a4j:jsFunction id="fCp_#{id}" name="searchCp_#{id}" immediate="true" render="#{idGroupVille}" limitRender="true"
            oncomplete="keeUtils.goToNextTabIndex( #{rich:element(idCp)} );">
            <a4j:param name="param1" assignTo="#{cp}" />
            <f:setPropertyActionListener value="#{cp}" target="#{cpVilleBean.cp}" />
            <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{ville}" />
        </a4j:jsFunction>

        <h:panelGroup layout="block" styleClass="cpGroup middle">
            <h:inputText id="#{idCp}" value="#{cp}" 
                maxlength="5" 
                styleClass="#{styleClass} cp smoothReadonly middle"
                readonly="#{readonly}"
                disabled="#{disabled}"
                onchange="#{onchange}" onfocus="#{onfocus}" onblur="#{onblur}" 
                required="#{required}" label="Cp #{label}"
                tabindex="#{tabindex}"
                onkeyup="if ($(event).which != 16){ if (keeUtils.cpOK(this.value)) { searchCp_#{id}(this.value); }}">
            </h:inputText>
        </h:panelGroup>
        <h:panelGroup layout="block" id="#{idGroupVille}" styleClass="villeGroup middle" >
                <h:inputText id="#{idVille}" value="#{ville}" 
                    style="width : #{villeWidth};" 
                    styleClass="#{styleClass} ville smoothReadonly"
                    rendered="#{disabled or not empty ville or empty cpVilleBean.villes or empty cp}"
                    readonly="#{readonly}"
                    disabled="#{disabled}"
                    onchange="#{onchange}"
                    onfocus="#{onfocus}"
                    onblur="#{onblur}"
                    required="#{required}" label="Ville #{label}"
                    tabindex="#{tabindexSup}" />

                <rich:autocomplete id="#{idVilles}" value="#{ville}" 
                    styleClass="middle"
                    inputClass="#{styleClass} comboVille"
                    rendered="#{(disabled)?'false': (not empty cpVilleBean.villes and empty ville and (jl:length(cp)==5))}" 
                    onchange="#{onchange}" onfocus="#{onfocus}" onblur="#{onblur}"
                    disabled="#{disabled}"
                    required="#{required}" label="Ville #{label}"
                    tabindex="#{tabindexSup}" 
                    mode="client" showButton="true" selectFirst="true" autofill="true" layout="div" 
                    autocompleteList="#{cpVilleBean.villes}" var="vil" fetchValue="#{vil.value}" >
                    #{vil.label}
                </rich:autocomplete>
        </h:panelGroup>
    </h:panelGroup>
</ui:composition>

Remarquez l'utilisation de l'opérateur ternaire qui me permet de spécifier des valeurs par défaut pour rendered, disabled, readonly, villeWidth et tabindexSup.

De plus pour simplifier la surcharge des styles de notre composant, j'ai créé une classe pour chaque élément qui reprend le contenu de l'attribut style. J'ai concaténé le paramètre styleClass à l'attribut styleClass de chaque élément. Les classes ont été externalisées dans un fichier CSS que voici :

/WebContent/resources/default/css/cpVille.css
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@CHARSET "UTF-8";
.middle { 
    vertical-align: middle;}
.cp {
    width:55px;
    margin:0px;
    vertical-align:middle;}
.cpGroup {
    display:inline-block;}
.ville {
    margin:0px;}
.villeGroup {
    display:inline-block;
    margin:0px 0px 0px 3px;}

Ce fichier est inclus dans la facelet grâce à la balise <h:outputStylesheet>.

Dans l'exemple de l'adresse pro, pour surcharger la longueur du combo ville je dois maintenant écrire l'instruction CSS suivante :

 
Sélectionnez
1.
2.
3.
<h:outputStylesheet>
    input.pro.comboVille { width: 186px; }
</h:outputStylesheet>

Enfin le code JavaScript utilisé est inclus directement dans la facelet via la balise <h:outputScript>. Voici d'ailleurs le code source de ce fichier :

/WebContent/resources/default/js/utils.js
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
var keeUtils = {
    cpOK : function(text) {
        return this.compareTextLength(text, 5) == 0 || this.compareTextLength(text, 0) == 0;
    },
    compareTextLength : function(text, length) {
        if (text.length > length)
            return 1;
        if (text.length < length)
            return -1;
        if (text.length == length)
            return 0;
    },
    goToNextTabIndex : function(currentElmt) {
        var ntabindex = parseFloat(currentElmt.getAttribute('tabindex'));
        ntabindex++;
        nbTentative = 0;
        var nextField = this.getFieldByTabIndex(ntabindex);
        while(nextField.length == 0 && nbTentative < 5) {
            ntabindex++;
            nextField = this.getFieldByTabIndex(ntabindex);
            nbTentative++;
        }
        if(nextField.length != 0) {
            nextField.focus();
        }
    },
    goToPrevTabIndex : function(currentElmt) {
        var ntabindex = parseFloat(currentElmt.getAttribute('tabindex'));
        ntabindex--;
        var prevField = this.getFieldByTabIndex(ntabindex);
        if(prevField.length != 0) {
            prevField.focus();
        }
    },
    getFieldByTabIndex : function (ntabindex) {
        var field = jQuery('input[tabindex='+ntabindex+']');
        if(field.length == 0) {
            field = jQuery('select[tabindex='+ntabindex+']');
            if (field.length == 0) {
                field = jQuery('textarea[tabindex='+ntabindex+']');
                if (field.length == 0) {
                    field = jQuery('a[tabindex='+ntabindex+']');
                }
            }
        }
        return field;
    }
}

V. Méthode 2 : Composant facelet composite

Pour créer le composant selon la deuxième méthode, nous allons créer un fichier dans le répertoire /WebContent/resources/composant.J'ai choisi « composant » de manière arbitraire, mais le dossier doit être dans le répertoire resources.

/WebContent/resources/composant/cpVille.xhtml
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:a4j="http://richfaces.org/a4j"
    xmlns:rich="http://richfaces.org/rich"
    xmlns:jl="http://java.sun.com/jsp/jstl/functions"
    xmlns:composite="http://java.sun.com/jsf/composite" >

    <composite:interface>

        <!-- Déclaration des attributs du composants -->

    </composite:interface>

    <composite:implementation>

        <!-- Code source du composant -->

    </composite:implementation>
</html>

La déclaration du namespace composite nous permet d'utiliser les éléments interface et implementation qui permettent respectivement de :

  • déclarer les attributs de notre composant ;
  • écrire le code du composant.

Voici le code résultant :

/WebContent/resources/composant/cpVille.xhtml
CacherSélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
    <composite:interface>
        <composite:attribute name="id" required="true" />
        <composite:attribute name="cp" required="true" type="java.lang.String" shortDescription="Bean Property for the cp value" />
        <composite:attribute name="ville" required="true" type="java.lang.String" shortDescription="Bean Property for the ville value" />
        <composite:attribute name="rendered" default="true" />
        <composite:attribute name="readonly" default="false"  />
        <composite:attribute name="disabled" default="false" />
        <composite:attribute name="required" default="false" />
        <composite:attribute name="label" />
        <composite:attribute name="onchange" />
        <composite:attribute name="onfocus" />
        <composite:attribute name="onblur" />
        <composite:attribute name="villeWidth" default="170px" />
        <composite:attribute name="styleClass" />
        <composite:attribute name="tabindex" />
    </composite:interface>
    <composite:implementation>
        <h:outputStylesheet library="default" name="css/cpVille.css" />
        <h:outputScript library="default" name="js/utils.js" target="head" />

        <h:panelGroup id="#{cc.attrs.id}" layout="block" rendered="#{cc.attrs.rendered}" styleClass="clearFloat middle">
            <ui:param name="idCp" value="#{cc.attrs.id}_cp" />
            <ui:param name="idVille" value="#{cc.attrs.id}_ville" />
            <ui:param name="idVilles" value="#{cc.attrs.id}_villes" />
            <ui:param name="idGroupVille" value="#{cc.attrs.id}_pgVille" />
            <ui:param name="idGroupVille" value="#{cc.attrs.id}_pgVille" />
            <ui:param name="tabindexSup" value="#{(cc.attrs.tabindex==null) ? '' : cc.attrs.tabindex + 1}" />
            <ui:param name="villeWidth" value="width : #{cc.attrs.villeWidth}" />

            <a4j:jsFunction id="fCp_#{id}" name="searchCp_#{id}" immediate="true" render="#{idGroupVille}" limitRender="true"
                oncomplete="keeUtils.goToNextTabIndex( #{rich:element(idCp)} );" >
                <a4j:param name="param1" assignTo="#{cp}" />
                <f:setPropertyActionListener value="#{cp}" target="#{cpVilleBean.cp}" />
                <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{ville}" />
            </a4j:jsFunction>

            <h:panelGroup layout="block" styleClass="cpGroup middle">
                <h:inputText id="#{idCp}" value="#{cc.attrs.cp}" 
                    maxlength="5" 
                    styleClass="#{cc.attrs.styleClass} cp smoothReadonly middle"
                    readonly="#{cc.attrs.readonly}"
                    disabled="#{cc.attrs.disabled}"
                    onchange="#{cc.attrs.onchange}" onfocus="#{cc.attrs.onfocus}" onblur="#{cc.attrs.onblur}" 
                    required="#{cc.attrs.required}" label="Cp #{cc.attrs.label}"
                    tabindex="#{cc.attrs.tabindex}"
                    onkeyup="if ($(event).which != 16){ if (keeUtils.cpOK(this.value)) { searchCp_#{id}(this.value); }}">
                </h:inputText>
            </h:panelGroup>
            <h:panelGroup layout="block" id="#{idGroupVille}" styleClass="villeGroup middle" >
                    <h:inputText id="#{idVille}" value="#{ville}" 
                        style="#{villeWidth};" 
                        styleClass="#{cc.attrs.styleClass} ville smoothReadonly"
                        rendered="#{cc.attrs.disabled or not empty cc.attrs.ville or empty cpVilleBean.villes or empty cc.attrs.cp}"
                        readonly="#{cc.attrs.readonly}"
                        disabled="#{cc.attrs.disabled}"
                        onchange="#{cc.attrs.onchange}"
                        onfocus="#{cc.attrs.onfocus}"
                        onblur="#{cc.attrs.onblur}"
                        required="#{cc.attrs.required}" label="Ville #{cc.attrs.label}"
                        tabindex="#{tabindexSup}" />

                    <rich:autocomplete id="#{idVilles}" value="#{ville}"
                        styleClass="middle"
                        inputClass="#{cc.attrs.styleClass} comboVille"
                        rendered="#{(cc.attrs.disabled) ? false : (not empty cpVilleBean.villes and empty cc.attrs.ville and (jl:length(cc.attrs.cp)==5))}" 
                        onchange="#{cc.attrs.onchange}" onfocus="#{cc.attrs.onfocus}" onblur="#{cc.attrs.onblur}"
                        disabled="#{cc.attrs.disabled}" required="#{cc.attrs.required}" label="Ville #{cc.attrs.label}"
                        tabindex="#{tabindexSup}" 
                        mode="client" showButton="true" selectFirst="true" autofill="true" autocompleteList="#{cpVilleBean.villes}" var="vil" layout="div" fetchValue="#{vil.value}">
                        #{vil.label}
                    </rich:autocomplete>
            </h:panelGroup>
        </h:panelGroup>
    </composite:implementation>

Comparée à la première méthode, cette syntaxe offre les avantages suivants en termes de lisibilité :

  • aperçu exhaustif de tous les attributs possibles en lisant la partie interface (<composite:attribute>) ;
  • revue rapide des attributs obligatoires via required ;
  • revue rapide des valeurs par défaut via default ;
  • revue rapide du type de donnée attendu pour les propriétés de bean via type ;
  • vue sur l'utilité de l'attribut via shortDescription.

Les attributs déclarés peuvent être référencés dans le code source implementation en utilisant la syntaxe #{cc.attrs.nom_attr}.

Pour utiliser le composant, il faut déclarer un namespace qui pointe vers le sous-dossier de resources dans lequel se trouve notre composant :

Namespace "kee"
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html
    xmlns="http://www.w3.org/1999/xhtml"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:rich="http://richfaces.org/rich"
    xmlns:a4j="http://richfaces.org/a4j"
    xmlns:kee="http://java.sun.com/jsf/composite/composant" >

Ensuite pour l'inclure dans la page, il faut simplement préfixer le nom du fichier par le namespace et déclarer les attributs requis et optionnels.

 
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
<h:panelGrid columns="2" >

    <h:outputLabel value="Adresse " for="perso_adresse" />
    <h:inputText id="perso_adresse" value="#{exempleCpVilleBean.persoAdresse}" 
        style="width: 300px"/>    

    <h:panelGroup>
        <h:outputLabel value="CP " for="perso_cp" />/
        <h:outputLabel value=" Ville" for="perso_ville" />
    </h:panelGroup>

    <kee:cpVille id="perso" 
        cp="#{exempleCpVilleBean.persoCp}" 
        ville="#{exempleCpVilleBean.persoVille}" 
        villeWidth="238px"
        styleClass="perso"
        tabindex="5"
        onchange="notifyChange()" />
</h:panelGrid>

Comparée à la première méthode, l'utilisation d'un composant composite nous fait bénéficier de l'autocomplétion lors de l'écriture du code et les descriptions des attributs shortDescriptionseront rendues lisibles par votre IDE.

Je reviens sur la syntaxe de définition du composant. Jusqu'ici, j'ai adapté mon composant résultant de la méthode par inclusion pour créer un composant composite. En fait, le fonctionnement des composants composites me permet de simplifier quelques déclarations.

  1. Attributs implicites
    Les attributs « id » et « rendered » n'ont pas besoin d'être déclarés et ne devraient pas l'être, car ils sont implicitement hérités du composant de base dont héritent tous les composants composites.
    Je peux néanmoins laisser la déclaration de « id » pour obliger le développeur à le définir.
    Pour utiliser l'id dans l'implémentation, il est possible d'utiliser la syntaxe #{cc.id}.
  2. Génération des id
    Tous les sous-composants JSF qui constituent le composant composite auront un id de la forme suivante :
    idFormulaire:idComposant:idSousComposant.
    Exemple : fCpVil:perso:cp
    Je n'ai donc plus besoin des quatre paramètres idCp, idVille, idVilles, idGroupeVille qui permettaient d'avoir des id uniques pour la méthode 1 par inclusion.

    Pour utiliser un label qui pointe vers un sous-composant, il faut définir son attribut for avec la syntaxe idComposant:idSousComposant.

    Exemple pour cp : <h:label value="CP :" for="perso:cp" />

  3. Référencement AJAX
    Du fait du point précédent, le div conteneur du composant ne portera pas l'id saisi. Sur l'exemple de « perso », il sera généré de la manière suivante : fCpVil:perso:perso.
    Du coup, dans la page principale, il n'est plus possible de faire référence au composant dans les requêtes Ajax via execute et render.
    Pour résoudre ce problème, il faut encapsuler la partie implémentation dans un <div> ou un <span> dont l'id est #{cc.clientId}.
    J'ai donc remplacé le premier h:panelGroup par un div.

Finalement, le code du composant est le suivant :

/WebContent/resources/composant/cpVille.xhtml
Sélectionnez
1.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"
    xmlns:f="http://java.sun.com/jsf/core"
    xmlns:ui="http://java.sun.com/jsf/facelets"
    xmlns:a4j="http://richfaces.org/a4j"
    xmlns:rich="http://richfaces.org/rich"
    xmlns:jl="http://java.sun.com/jsp/jstl/functions"
    xmlns:fct="http://www.alladin.fr/socaf"
    xmlns:composite="http://java.sun.com/jsf/composite" >

    <composite:interface>
        <composite:attribute name="id" required="true" />
        <composite:attribute name="cp" required="true" type="java.lang.String" shortDescription="Bean Property for the cp value" />
        <composite:attribute name="ville" required="true" type="java.lang.String" shortDescription="Bean Property for the ville value" />
        <composite:attribute name="readonly" default="false"  />
        <composite:attribute name="disabled" default="false" />
        <composite:attribute name="required" default="false" />
        <composite:attribute name="label" />
        <composite:attribute name="onchange" />
        <composite:attribute name="onfocus" />
        <composite:attribute name="onblur" />
        <composite:attribute name="villeWidth" default="170px" />
        <composite:attribute name="styleClass" />
        <composite:attribute name="tabindex" />
    </composite:interface>
    <composite:implementation>
        <h:outputStylesheet library="default" name="css/cpVille.css" />
        <h:outputScript library="default" name="js/utils.js" target="head" />
        
        <div id="#{cc.clientId}" class="clearFloat middle" >
            
            <ui:param name="tabindexSup" value="#{(cc.attrs.tabindex==null) ? '' : cc.attrs.tabindex + 1}" />
            <ui:param name="villeWidth" value="width : #{cc.attrs.villeWidth}" />
            
            <a4j:jsFunction id="fCp" name="searchCp_#{cc.id}" immediate="true" render="pgVille" limitRender="true"
                oncomplete="keeUtils.goToNextTabIndex( #{rich:element('cp')} );" >
                <a4j:param name="param1" assignTo="#{cc.attrs.cp}" />
                <f:setPropertyActionListener value="#{cc.attrs.cp}" target="#{cpVilleBean.cp}" />
                <f:setPropertyActionListener value="#{cpVilleBean.ville}" target="#{cc.attrs.ville}" />
            </a4j:jsFunction>
    
            <h:panelGroup layout="block" styleClass="cpGroup middle">
                <h:inputText id="cp" value="#{cc.attrs.cp}" 
                    maxlength="5" 
                    styleClass="#{cc.attrs.styleClass} cp smoothReadonly middle"
                    readonly="#{cc.attrs.readonly}"
                    disabled="#{cc.attrs.disabled}"
                    onchange="#{cc.attrs.onchange}" onfocus="#{cc.attrs.onfocus}" onblur="#{cc.attrs.onblur}" 
                    required="#{cc.attrs.required}" label="Cp #{cc.attrs.label}"
                    tabindex="#{cc.attrs.tabindex}"
                    onkeyup="if ($(event).which != 16){ if (keeUtils.cpOK(this.value)) { searchCp_#{cc.id}(this.value); }}">
                </h:inputText>
            </h:panelGroup>
            <h:panelGroup layout="block" id="pgVille" styleClass="villeGroup middle" >
                    <h:inputText id="ville" value="#{cc.attrs.ville}" 
                        style="#{villeWidth};" 
                        styleClass="#{cc.attrs.styleClass} ville smoothReadonly"
                        rendered="#{cc.attrs.disabled or not empty cc.attrs.ville or empty cpVilleBean.villes or empty cc.attrs.cp}"
                        readonly="#{cc.attrs.readonly}"
                        disabled="#{cc.attrs.disabled}"
                        onchange="#{onchange}"
                        onfocus="#{onfocus}"
                        onblur="#{onblur}"
                        required="#{cc.attrs.required}" label="Ville #{cc.attrs.label}"
                        tabindex="#{tabindexSup}" />
    
                    <rich:autocomplete id="villes" value="#{cc.attrs.ville}"
                        styleClass="middle"
                        inputClass="#{cc.attrs.styleClass} comboVille"
                        rendered="#{(cc.attrs.disabled) ? false : (not empty cpVilleBean.villes and empty cc.attrs.ville and (jl:length(cc.attrs.cp)==5))}" 
                        onchange="#{cc.attrs.onchange}" onfocus="#{cc.attrs.onfocus}" onblur="#{cc.attrs.onblur}"
                        disabled="#{cc.attrs.disabled}" required="#{cc.attrs.required}" label="Ville #{cc.attrs.label}"
                        tabindex="#{tabindexSup}" 
                        mode="client" showButton="true" selectFirst="true" autofill="true" autocompleteList="#{cpVilleBean.villes}" var="vil" layout="div" fetchValue="#{vil.value}">
                        #{vil.label}
                    </rich:autocomplete>
            </h:panelGroup>
        </div>
    </composite:implementation>
</html>

VI. Conclusion

Voilà, nous avons bouclé cet article sur la création d'un composant en JSF.

Après avoir expliqué le fonctionnement du composant exemple CP/Ville, nous avons listé les paramètres, puis créer le composant de deux manières différentes : d'une part en utilisant le principe d'inclusion de facelet et d'autre part en utilisant le principe des composants composites (composite components).

Cependant, je ne peux utiliser le composant sans inclure CpVilleBean dans mon projet. Afin de déporter la logique métier dans le composant, il faudrait utiliser le principe de NamingContainer que je développerai dans un futur tutoriel.

Ressources :

VII. Remerciements

Cet article a été publié avec l'aimable autorisation de la société keeleekkeeleek et l'article d'origine (tutoriel-creation-composant-jsf-cp-ville) peut être vu sur le blog/site de keeleekkeeNews.

Nous tenons à remercier Mickael Baron pour la mise au gabarit, Olivier Butterlin pour la relecture technique et Claude Leloup pour la relecture orthographique.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2015 Franck Gasparotto. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.