Création d'un composant facelet personnalisé avec Facelets JSF et Richfaces

Mikael Robert

Si vous utilisez JSF, vous avez dû remarquer qu'on fait souvent des assemblages similaires de composants.

Parfois l'idée vous passe par la tête de créer votre propre composant JSF pour remplacer ces assemblages redondants, mais créer un composant JSF est long et difficile.
La difficulté réside notamment dans la gestion du cycle de vie JSF du composant que vous créez.
Bien heureusement il existe une solution autre grâce à Facelets
En effet vous pouvez créer un assemblage de composants JSF qui deviendra alors un composant en soi, mais un composant Facelet cette fois.
Ce qui ne changera en rien son utilisation.

- Titre : Création d'un composant facelet personnalisé avec Facelets JSF et Richfaces

- Auteur : Mikael Robert

- Parution : 11 mars 2009

- Mise à jour : 17 février 2020

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2009 Mikael Robert. Aucune reproduction, même partielle, ne peut être faite de ce site et 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.

Sommaire

- I. Introduction

- II. Les étapes

- III. Configuration

- IV. Taglib

- V. Création du composant

- VI. Utilisation du composant

I - Introduction

Le but de ce tutoriel est de créer une list box qui peut recevoir n'importe quoi dans la liste déroulante, dans mon cas j'ai besoin de listes ou d'arbres de checkbox avec label.
J'ai donc besoin d'utiliser dans ma liste déroulante, soit une rich:dataTable soit un rich:tree.

Je vous renvoie au Tutoriel de L. Mellouk sur richfaces pour l'utilisation du rich:tree ou de la rich:dataTable.
On souhaite arriver à un composant générique qui permettrait de faire ce genre de choses :

Le composant avec une rich:dataTable

Le composant avec un rich:tree

II - Les étapes

Créer un composant Facelets requiert trois étapes :

- création d'un fichier de taglib pour pouvoir le déclarer à facelets et l'utiliser ;

- déclaration du fichier de taglib à facelets dans le web.xml ;

- création du composant dans un fichier xhtml.

III - Configuration

Le plus simple pour intégrer les composants que vous créez à votre WAR est de les placer dans le répertoire WEB-INF.
Par exemple j'ai créé un répertoire components dans WEB-INF.
Voyons maintenant la configuration du composant, tout d'abord dans web.xml :
on indique à Facelets d'aller chercher les composants additionnels dans WEB-INF/facescomponents.taglib.xml :

Ajout dans web.xml

<context-param>
<param-name>facelets.LIBRARIES</param-name>
<param-value>/WEB-INF/facescomponents.taglib.xml</param-value>
</context-param>

IV - Taglib

Ensuite, comme vous pouvez le voir précédemment, on crée un fichier taglib pour notre composant : voici le fichier facescomponents.taglib.xml :

facescomponents.taglib.xml

<?xml version="1.0"?>
<!DOCTYPE facelet-taglib PUBLIC
"-//Sun Microsystems, Inc.//DTD Facelet Taglib 1.0//EN"
"facelet-taglib_1_0.dtd">
<facelet-taglib>
<namespace>http://www.dreamisle.net/facescomponents</namespace>
<tag>
<tag-name>selectListCheckBox</tag-name>
<source>components/selectListCheckBox.xhtml</source>
</tag>
</facelet-taglib>

V - Création du composant

On peut maintenant créer le composant.

J'ai fourni CSS/JavaScript volontairement au cas où vous souhaitiez réutiliser ce composant.
Néanmoins, tout ce qui correspond à la position et au z-index est important pour permettre la création de la pseudo liste déroulante.

Le JavaScript (ici fait avec jQuery) est utilisé pour enrouler/dérouler la liste, et pour compter les checkbox cochées dans le contenu.
Il s'agit d'un exemple de comportement JavaScript que l'on peut attribuer à un composant, à vous d'adapter à votre choix. J'ai choisi d'utiliser jQuery pour plus de portabilité.
De plus la lbibliothèque permet d'éviter de polluer le code jsf d'appels JavaScript sur les “onclick”, “onchange”, etc.

Vous pouvez bien sûr définir ce que vous voulez dans le comportement de votre composant, voire contrôler le composant avec un objet Seam appelé en ajax, mais cela le rend inutilisable en dehors de votre contexte Seam.
Le système d'id est un peu compliqué, en fait, on crée nos id à partir de l'id du composant créé dans la JSF, pour éviter les erreurs de duplicate ID.
Donc les id sont faits avec des EL, mais ne vous formalisez pas de cela.
Cela complexifie la lecture du code jQuery et JSF, mais c'est une façon simple d'éviter le duplicate ID si vous insérez plusieurs fois le composant dans la même page.

le composant en lui-même : selectListCheckBox.xhtml

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
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:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j">

<!-- Style-->
<style>

/* on met le composant en position relative, pour pouvoir placer la liste déroulante au-dessus en absolute après*/
.divLayoutSelectListCheckBox {
position: relative;
}
/* la largeur totale du composant*/
.dataGridLayoutSelectListCheckBox {
width:350px
}
/* le champ input simulant une select box */
.idropDownSelectListCheckBoxInput {
width:332px;
margin:0px;
z-index: 2;
}
/* uniquement pour que la rich:dataTable n'ait pas de bordures. */
.dataTableDropDownList {
width: 350px;
border-style: none;
background-color: #EEF5FE;
}
/* uniquement pour que les colonnes de la rich:dataTable n'aient pas de bordures. */
.dataTableDropDownListColumn {
border-bottom: 0px;
border-right: 0px;
}
/* le div de la liste en elle-même. */
.dropDownSelectListCheckBox {
height: 202px;
overflow: auto;
background-color: #EEF5FE;
border: 1px solid #CAC5BE;
}
/* on fixe en position absolue et avec un z-index important  le div entourant la liste déroulante pour qu'elle passe au-dessus du reste*/
.fullListDropDownCheckBox {
position: absolute;
z-index: 5;
}
/* le pied de la liste déroulante*/
.dropDownSelectListCheckBoxFooter {
background-color: #BED6F8;
border: 1px solid #CAC5BE;
}

</style>
<!-- Javascript for the component behaviour-->
<a4j:loadScript src="resource://jquery.js"/>
<script type="text/javascript">
jQuery(document).ready(function() {
/* par défaut on cache la liste déroulante */
jQuery("##{id}magicBoxList").hide();

/* si le paramètre footer est à faux on cache le footer*/
if (!jQuery(#{footer})) {
jQuery("##{id}footerListBox").hide();
}
/* on remplit la valeur du champ input par défaut */
jQuery("##{formId}\\:#{id}statesinput").attr("value", "0" + " " + '#{selectedLabel}');

/* un clic dans l'input cache ou montre la liste*/
jQuery("##{formId}\\:#{id}statesinput").click(function() {
if (jQuery("##{id}magicBoxList").is(":hidden")) {
jQuery("##{id}magicBoxList").show();
} else {
jQuery("##{id}magicBoxList").hide();
}
});
/* un clic sur la flèche cache ou montre la liste.*/
jQuery("##{formId}\\:#{id}showandhidelinkid").click(function() {
if (jQuery("##{id}magicBoxList").is(":hidden")) {
jQuery("##{id}magicBoxList").show();
} else {
jQuery("##{id}magicBoxList").hide();
}
});

/* désélectionne toutes les checkboxes lorsqu'on clique sur le lien correspondant*/
jQuery('##{formId}\\:#{id}deselectAllButtonFooter').click(function(){
jQuery("##{id}dropListContainer input[@type='checkbox']").attr('checked', false);
jQuery("##{formId}\\:#{id}statesinput").attr("value", "0 " + '#{selectedLabel}');
});

/*  met  à jour le compteur de cases cochées à chaque clic sur une checkbox  */
jQuery("##{id}dropListContainer input[@type='checkbox']").click(function() {
count = jQuery("##{id}dropListContainer  input[@type='checkbox']:checked").length;
if (count > #{maxCheck}) {
jQuery("##{formId}\\:#{id}statesinput").attr("value", "#{maxSelectMessage}" + "  " + "#{maxCheck}" + " éléments");
jQuery(this).attr('checked', false);
} else {
jQuery("##{formId}\\:#{id}statesinput").attr("value", count + " " + '#{selectedLabel}');
}
});
});
</script>

<!-- le code proprement dit du composant -->
<h:panelGrid columns="2">
<s:div styleClass="divLayoutSelectListCheckBox" id="#{id}idPanelGridForFc">
<h:panelGrid columns="2" border="0" cellpadding="0" cellspacing="0"
styleClass="dataGridLayoutSelectListCheckBox" >
<!-- on imite une select box avec un inputText désactivé et une image de flèche -->
<h:inputText id="#{id}statesinput" readonly="true"
styleClass="idropDownSelectListCheckBoxInput" />
<a4j:commandLink id="#{id}showandhidelinkid">
<h:graphicImage value="/img/arrow.png" alt="&gt;"/>
</a4j:commandLink>
</h:panelGrid>
<h:panelGroup>
<!-- la liste déroulante-->
<div class="fullListDropDownCheckBox" id="#{id}magicBoxList">
<div class="dropDownSelectListCheckBox" id="#{id}dropListContainer">
<!-- le contenu de la liste déroulante sera inséré ici-->
<ui:insert />
</div>
<!-- on ajoute un footer à la liste déroulante avec un lien  "tout déselectionner"-->
<div class="dropDownSelectListCheckBoxFooter" id="#{id}footerListBox">
<a4j:commandLink id="#{id}deselectAllButtonFooter">
Tout désélectionner
</a4j:commandLink>
</div>
</div>
</h:panelGroup>
</s:div>
</h:panelGrid>

</ui:composition>

Comme vous pouvez le voir, on a créé le composant de la même manière que l'on écrirait une page jsf classique, la différence réside uniquement dans le fait que l'on crée une composition.

L'élément important ici est le <ui:insert/>
C'est ici que sera placé le contenu de la liste déroulante, remplie par l'utilisateur du composant dans une page JSF.
Autre élément à noter : les EL utilisées dans le composant.
Par exemple ici #{id} : il s'agit en fait de récupérer les paramètres, passés au composant de manière classique dans la JSF.
Pour mieux comprendre lisez la partie “utilisation du composant” vous allez tout de suite voir que les paramètres déclarés au composant, sont récupérables sous forme d'EL avec la même casse que dans l'EL, ainsi votre composant est facilement paramétrable.

#{formId}, #{maxSelectMessage}, #{maxCheck}, #{footer} et #{selectedLabel} ne sont utilisés que dans les fonctions de comportement jQuery du composant.
Vous voyez que vous pouvez même utiliser les paramètres pour définir le comportement du composant avec JavaScript.

J'ai choisi pour l'exemple d'utiliser des a4j:commandLink liés à des appels JavaScript (déclarés dans la partie JavaScript), mais il y a bien d'autres façons de faire.

VI - Utilisation du composant

Et voici maintenant un exemple d'utilisation qui permet d'arriver aux captures du début de cet article :

<!DOCTYPE composition PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:s="http://jboss.com/products/seam/taglib"
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:rich="http://richfaces.org/rich"
xmlns:a4j="http://richfaces.org/a4j"
xmlns:fc="http://www.meteojob.com/facescomponents"
template="layout/template.xhtml">

<ui:define name="body">
<rich:panel>
<f:facet name="header"> select test </f:facet>
<h:form id="formRichListId">
<!-- ici une liste avec en label une chaine de caracteres, et devant le label une checkbox -->
<fc:selectListCheckBox id="fcSelectList" formId="formRichListId" selectedLabel="éléments choisis"
maxCheck="4" maxSelectMessage="Vous devez choisir au maximum:" footer="true">
<rich:dataTable value="#{dreamSelectBox.theList}" var="item"
styleClass="dataTableDropDownList" >
<rich:column styleClass="dataTableDropDownListColumn">
<h:selectBooleanCheckbox value="false" name="checkboxRichList" />
</rich:column>
<rich:column styleClass="dataTableDropDownListColumn">
<h:outputText value="#{item}" escape="false" />
</rich:column>
</rich:dataTable>
</fc:selectListCheckBox>

<rich:spacer height="50" />
<!-- ici un arbre avec aussi des checkboxes, j'ai
repris l'arbre donné en exemple dans la démo en ligne de richfaces -->
<fc:selectListCheckBox id="fcSelectTree" formId="formRichListId"
selectedLabel="éléments cochés"
maxCheck="4" maxSelectMessage="Vous devez choisir au maximum:" footer="true">
<rich:tree style="width:350px;" nodeSelectListener="#{dreamSelectBox.theTree.processSelection}"
reRender="selectedNode" ajaxSubmitSelection="true"  switchType="client"
value="#{dreamSelectBox.theTree.getTreeNode()}" var="item" ajaxKeys="#{null}" >
<rich:treeNode>
<f:facet name="iconLeaf"><h:selectBooleanCheckbox value="false"/></f:facet>
<h:outputText value="#{item}" />
</rich:treeNode>
</rich:tree>
</fc:selectListCheckBox>

</h:form>
</rich:panel>
</ui:define>

</ui:composition>

Et voilà, vous avez votre composant, j'espère que cet article aura été utile.