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 :
- Via le principe d'include de facelet ;
- 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
Voici un échantillon de données :
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 :
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.
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 :
Après factorisation, suivant les deux méthodes que je vais vous présenter, nous obtiendrons :
Voici un comparatif visuel avant/après :
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 :
- Dupliquer les éléments du composant dans la même page via un copier/coller du code ;
- Le rendre fonctionnel en modifiant un minimum de code ;
- Comparer les éléments qui diffèrent ;
- 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).
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/
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 :
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 :
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 :
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.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
@CHARSET
"UTF-8"
;
.middle
{
vertical-align:
middle
;}
.cp
{
width:
55
px;
margin:
0
px;
vertical-align:
middle
;}
.cpGroup
{
display:
inline-block
;}
.ville
{
margin:
0
px;}
.villeGroup
{
display:
inline-block
;
margin:
0
px 0
px 0
px 3
px;}
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 :
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 :
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.
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 :
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 :
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.
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.
- 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}. -
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" />
- 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 :
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.