Champ de saisie "autocomplete" en JSF

Les champs de saisie intelligents proposant instantanément des suggestions de valeurs dès la saisie des premières lettres d'un mot sont de plus en plus répandus et offrent une expérience très appréciée des utilisateurs.

Je vous propose de suivre dans cet article une introduction à la mise en oeuvre de ce champ de saisie en JSF avec l'aide de la librairie PrimeFaces et de son composant autoComplete.

Nous découvrirons que l'intégration d'une librairie de composants JSF prêts à l'emploi telle que PrimesFaces, RichFaces ou encore ICEFaces, facilite grandement le travail du développeur dans la réalisation de pages web dynamiques basées sur la technologie AJAX.

La plateforme technique utilisée pour rédiger cet article est constituée du JDK version 1.6, du serveur GlassFish version 3.1.2 qui inclut la librairie d'implémentation JSF Mojarra version 2.1.6 et de la librairie PrimeFaces version 3.4

Contexte d'utilisation d'un champ autocomplete

autocomplete sampleImage 1 : exemple de champ autocomplete

Les champs de type autocomplete sont particulièrement adaptés à la saisie de données limitées à un sous-ensemble de valeurs pré-définies ou encore à la saisie de données de recherche.

En effet, dans le premier cas, la saisie est orientée en fournissant à l'utilisateur une liste de valeurs autorisées se rapprochant des premières lettres qu'il a saisies.

Ce cas correspond par exemple à la saisie du nom d'une ville, d'un client ou encore d'un article à commander.

Dans le cas d'une recherche, l'approche est un peu différente puisque l'utilisateur se voit suggérer une liste de valeurs relative à son début de saisie, sans pour autant le contraindre à suivre les propositions qui lui sont faîtes.

Le cas du champ de recherche de Google est un bon exemple : des mots clés sont suggérés en fonction de multiples critères (localisation géographique de l'internaute, fréquence des mots clés demandés, etc...) mais l'utilisateur reste finalement libre de saisir les mots clé de son choix et de lancer la recherche selon ses propres critères.

A noter que de manière générale, les données à saisir dans un champ autocomplete ne doivent pas être codifiées.
Elles doivent représenter des informations qui ont du sens pour l'utilisateur et être adaptées à son langage naturel.
Autrement, il est préférable pour la saisie de valeurs codifiées (par exemple un code client), d'adjoindre au champ de saisie un accès à une liste des codes incluant des fonctionnalités de recherche sur des critères autres que des valeurs codifiées (nom, adresse...).

Le composant autoComplete de la librairie RichFaces

La librairie RichFaces inclut le composant nommé autoComplete à la fois très ouvert en termes de personnalisation et très facile à intégrer dans une page JSF.
Une démonstration de ce composant est disponible sur le site officiel www.primefaces.org.

Son implémentation la plus simple consiste à :

  1. Déclarer le composant Autocomplete dans la page JSF à l'aide de la balise <p:autoComplete />,
    <?xml version='1.0' encoding='UTF-8' ?>
    <!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:h="http://java.sun.com/jsf/html"
          xmlns:p="http://primefaces.org/ui">
            <h:head>
                <title>Démonstration AutoComplete PrimeFaces</title>
            </h:head>
            <h:body>
                <h1>Recherche d'un Client</h1>
                <h:form id="form">
                    <h:outputLabel value="Nom du client : " for="clientName" />
                    <p:autoComplete id="clientName" value="#{autoCompleteClientBean.selectedClient}"   
                                completeMethod="#{autoCompleteClientBean.complete}" />
                </h:form>
            </h:body>
    </html>
    Code 1 : exemple de déclaration du composant autoComplete PrimeFaces dans une vue Facelet.
  2. Créer un managed bean associé disposant à minima :
    • D'une propriété contenant la valeur saisie dans le champ,
    • D'une méthode chargée de fournir les suggestions de valeurs au composant à partir du texte saisi dans le champ.
    package managedbeans;
     
    import java.util.ArrayList;
    import java.util.List;
    import javax.faces.bean.ManagedBean;
     
    @ManagedBean
    public class AutoCompleteClientBean {
        private String selectedClient;
     
        public List<String> complete(String query) {  
            List<String> results = new ArrayList<String>();  
            for (int i = 0; i < 10; i++) {  
                results.add(query + i);  
            }  
            return results;  
        } 
     
        public String getSelectedClient() {
            return selectedClient;
        }
     
        public void setSelectedClient(String selectedClient) {
            this.selectedClient = selectedClient;
        }
    }
    Code 2 : exemple de managed bean associé au composant autoComplete PrimeFaces.

Une autre implémentation possible consiste à relier directement le composant autoComplete aux objets métier de l'application exposés sous forme de POJO, par l'intermédiaire d'un Convertisseur personnalisé, chargé de convertir le texte saisi en objet et inversement.

Dans le paragraphe suivant, nous nous focaliserons sur l'implémentation d'un champ autoComplete dont les suggestions seront proposées à partir d'une liste d'objets.

L'application de démonstration demo_autocomplete

Les fonctionnalités à développer

L'application web de démonstration que je vous propose de développer permet de consulter les informations détaillées d'un client dont le nom est saisi au préalable dans un champ autocomplete.
L'utilisateur dispose évidemment d'une assistance à la saisie; il se voit proposer une liste de clients correspondant aux premières lettres qu'il a entrées.
A la validation du formulaire, l'utilisateur est dirigé vers une page affichant les informations détaillées du client sélectionné.

autocomplete demo pagesImage 2 : pages de l'application web demo_autocomplete

Les clients sont structurés dans l'application sous forme de POJO créés statiquement pour l'exemple.

Ces POJO pourraient dans une application complète, correspondre à des entités de type JPA.

autocomplete netbeans projectImage 3 : projet demo_autocomplete sous NetBeans

Le code source de cette application de démonstration comprend les fichiers suivants :

  • web.xml : descripteur de déploiement de l'application de démonstration. Son contenu généré en standard est suffisant pour les besoins de la démonstration.
  • glassfish-web.xml : descripteur de déploiement spécifique à GlassFish dans lequel est précisé le jeu de caractères UTF-8 pour les besoins de PrimeFaces à l'encodage des vues.
  • index.xhtml : vue Facelet d'affichage d'un champ de saisie et d'un bouton de validation du client renseigné.
  • result.xhtml : vue Facelet d'affichage des informations du client sélectionné à la page index.xhtml.
  • AutoCompleteClientBean.java : managed bean connecté au champ de saisie autoComplete disposant d'une propriété pour mémoriser le client sélectionné, d'une méthode fournissant les suggestions de noms de clients et enfin, d'une méthode appelée à la validation du formulaire (action du bouton Submit).
  • Client.java : POJO décrivant les propriétés d'un client.
  • ClientModel.java : classe d'accès aux données des clients .
  • ClientConverter.java : convertisseur personnalisé chargé de transtyper les données objets du client en données textes affichables dans la vue Facelet.

L'application a été générée avec NetBeans à partir du menu File | New Project... en renseignant les étapes suivantes :

  1. Etape Choose Project : Categories: Java Web Projects: Web Application,
  2. Etape Name and Location : Project Name: demo_autocomplete,
  3. Etape Server and Settings : Server: GlassFish Server 3+ Java EE Version: Java EE 6 Web,
  4. Etape Frameworks : Framework: JavaServer Faces Libraries: JSF 2.1 Components: PrimeFaces.

La vue index.xhtml

Il s'agit de la vue Facelet affichée par défaut.
Elle affiche un formulaire <h:form/> incluant une instance du composant <p:autoComplete/> de saisie du nom du client recherché et un bouton d'action <h:commandButton/> pour valider le client sélectionné.

<?xml version='1.0' encoding='UTF-8' ?>
<!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:h="http://java.sun.com/jsf/html"
      xmlns:p="http://primefaces.org/ui">
        <h:head>
            <title>PrimeFaces AutoComplete Demonstration</title>
        </h:head>
        <h:body>
            <h1>Client search...</h1>
            <p>Enter the Client's name for displaying his informations.</p>
            <p>For example : Renault, Singer, Canon, Orange, Lyonnaise des eaux...</p>
            <h:form id="form">
                <h:outputLabel value="Client's name : " for="clientName" />
                <p:autoComplete id="clientName"
                        value="#{autoCompleteClientBean.selectedClient}"   
                        completeMethod="#{autoCompleteClientBean.complete}" 
                        converter="clientConverter"
                        var="c"
                        itemLabel="#{c.name}"
                        itemValue="#{c}"
                        forceSelection="true"
                        required="true"/>&nbsp;
                <h:commandButton value="Submit" action="#{autoCompleteClientBean.submit}"/>
            </h:form>
        </h:body>
</html>
Code 3 : vue index.xhtml de l'application demo_autocomplete.

Arrêtons-nous en particulier sur les attributs renseignés pour le composant autoComplete :

  • value="#{autoCompleteClientBean.selectedClient}" : expression EL qui référence l'objet selectedClient du managed bean AutoCompleteClientBean. En effet, l'échange de données entre la vue et le managed bean est assuré par un objet de type Client dont sa transformation texte / objet est assurée par le convertisseur indiqué pour l'attribut converter.
  • completeMethod="#{autoCompleteClientBean.complete" : il indique la méthode du managed bean à invoquer pour obtenir la liste des suggestions à partir des lettres saisies dans le composant.
  • converter="clientConverter" identifiant du convertisseur appelé pour disposer de l'objet de type Client correspondant au client sélectionné dans le composant.
  • var="c" nom donné à l'itérateur utilisé pour parcourir les suggestions basées sur les objets de type Client. Ce nom est utilisé par les attributs itemLabel et itemValue pour désigner l'objet Client courant de l'itérateur.
  • itemLabel="#{c.name}" libellé de l'objet Client qui est affiché pour chaque suggestion dans la liste déroulante.
  • itemValue="#{c}" valeur de la suggestion, en l'occurence l'objet Class correspondant à la suggestion.
  • forceSelection="true" oblige l'utilisateur à sélectionner un client parmi les suggestions proposées, autrement vide le champ à la perte du focus.

La vue result.xhtml

Elle a pour seule fonction d'afficher les informations du client sélectionné dans la vue index.xhtml.

Les informations du client sont obtenues à partir de l'objet Client contenu dans la propriété selectedClient du managed bean.

Cette vue est appelée par la méthode submit() du managed bean.

<?xml version='1.0' encoding='UTF-8' ?>
<!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:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Selected Client...</title>
    </h:head>
    <h:body>
        <h1>Selected Client...</h1>
        <p>
            <br/>Code: <strong>#{autoCompleteClientBean.selectedClient.clientCode}</strong>
            <br/>Name: <strong>#{autoCompleteClientBean.selectedClient.name}</strong>
            <br/>Address: <strong>#{autoCompleteClientBean.selectedClient.address}</strong>
            <br/>City: <strong>#{autoCompleteClientBean.selectedClient.city}</strong>
        </p>
        <p>Back to the <h:link outcome="index?faces-redirect=true">main page</h:link></p>
    </h:body>
</html>
Code 4 : vue result.xhtml de l'application demo_autocomplete.

Le managed bean AutoCompleteClientBean.java

Il fournit au composant autoComplete via un appel de la méthode List<Client> complete(String query), la liste des suggestions pour la chaîne de caractères saisie. La recherche des clients à retourner en guise de suggestions est déléguée à la classe ClientModel.

Il mémorise dans la propriété selectedClient l'objet Client sélectionné à partir du composant autoComplete.

Enfin, le managed bean dispose de la méthode submit() invoquée en réponse à l'action du bouton Submit et chargée simplement d'appeler la vue result.xhtml.

package managedbeans;
 
import entities.Client;
import java.util.List;
import javax.faces.bean.ManagedBean;
import model.ClientModel;
 
@ManagedBean
public class AutoCompleteClientBean {
    private Client selectedClient;
 
    public List<Client> complete(String query) {  
        return ClientModel.getClientsByName(query);  
    } 
 
    public Client getSelectedClient() {
        return selectedClient;
    }
 
    public void setSelectedClient(Client selectedClient) {
        this.selectedClient = selectedClient;
    }
 
    public String submit()
    {
        System.out.println("Selected client: " + this.selectedClient.getName());
        return "result";
    }
}
Code 5 : managed bean AutoCompleteClientBean.java de l'application demo_autocomplete.

Le convertisseur ConvertisseurClient.java

Ce convertisseur personnalisé implémente l'interface javax.faces.convert.Converter et ses deux signatures de méthode suivantes :

  • String getAsString(FacesContext context, UIComponent component, Object value) : cette méthode est appelée à la construction de la liste des suggestions pour disposer du code client au format texte pour chaque objet Client retourné par la méthode AutoCompleteClientBean.complete(String query).
  • Object getAsObject(FacesContext context, UIComponent component, String value) : elle est appelée après validation du formulaire de la vue index.xhtml (clic du bouton Submit). Elle permet d'obtenir l'objet Client à partir du code au format texte du client sélectionné dans le composant et ainsi, d'initialiser la propriété selectedClient du managed bean AutoCompleteClientBean.
package converters;
 
import entities.Client;
import javax.faces.component.UIComponent;
import javax.faces.context.FacesContext;
import javax.faces.convert.Converter;
import javax.faces.convert.FacesConverter;
import model.ClientModel;
 
@FacesConverter("clientConverter")
public class ClientConverter implements Converter {
    @Override
    public Object getAsObject(FacesContext context, UIComponent component, String value) {
        return ClientModel.getClient(value);  
    }
 
    @Override
    public String getAsString(FacesContext context, UIComponent component, Object value) {
        if (value == null || value.equals("")) {  
            return "";  
        } else {  
            return String.valueOf(((Client) value).getClientCode());  
        }  
    }
}

L'annotation @FacesConverter("clientConverter") permet de déclarer le convertisseur dans le conteneur GlassFish et de lui attribuer l'identifiant clientConverter référencé par le composant autoComplete à travers son attribut converter.

Le POJO Client.java

POJO utilisé pour transmettre à la vue les informations du client.

package entities;
 
public class Client {
    private String clientCode;
    private String name;
    private String address;
    private String city;
 
    public Client(String clientCode, String name, String address, String city) {
        this.clientCode = clientCode;
        this.name = name;
        this.address = address;
        this.city = city;
    }
 
    public String getClientCode() {
        return clientCode;
    }
 
    public void setClientCode(String codeClient) {
        this.clientCode = codeClient;
    }
 
    public String getName() {
        return name;
    }
 
    public void setName(String nom) {
        this.name = nom;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String adresse) {
        this.address = adresse;
    }
 
    public String getCity() {
        return city;
    }
 
    public void setCity(String ville) {
        this.city = ville;
    }
}
Code 6 : POJO Client.java de l'application demo_autocomplete.

La classe ClientModel.java d'accès aux objets Client

Cette classe contient la liste statique static final ArrayList<Client> clients des clients nécessaires à la démonstration.

D'autre part, elle dispose de la méthode Client getClient(String clientCode) appelée par le convertisseur pour obtenir l'objet Client correspondant à un code client donné.

Enfin, elle comprend la méthode List<Client> getClientsByName(String pieceOfName) invoquée par la méthode complete(String query) du managed bean pour obtenir la liste des objets Client dont le nom commence par les lettres indiquées en paramètre.
A noter que l'identification des clients ne tient pas compte de la casse des caractères. Les chaînes texte à comparer sont au préalable transformées en minuscules (utilisation de la méthode String.toLowerCase()), ce qui permet de retrouver les clients commençant par une lettre majuscule alors que la première lettre est saisie en minuscule.

package model;
 
import entities.Client;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
 
public class ClientModel {
    private static final ArrayList<Client> clients = 
	new ArrayList<Client>(Arrays.asList(
            new Client("CLI001", "Renault","16 allée des Peupliers","Tourcoing"),
            new Client("CLI002", "Singer","87 avenue des Platanes","Paris"),
            new Client("CLI003", "Canon","125 rue des Fraises","Marseille"),
            new Client("CLI004", "Orange","36 allée des Templiers","Orleans"),
            new Client("CLI005", "SFR","43 impasse du Baron","Nantes"),
            new Client("CLI006", "Lyonnaise des eaux","58 tour des Cieux","Montpellier"),
            new Client("CLI007", "Veolia","63 rue des Allouettes","Avignon"),
            new Client("CLI008", "Avensys","17 allée des Pigeons","Toulouse"),
            new Client("CLI009", "Cultura","2 avenue des Amandiers","Angers"),
            new Client("CLI010", "Histoire d'or","55 tour des Moulins","Mulhouse"),
            new Client("CLI011", "Cap Gemini","654 avenue du Fond","Perpignan"),
            new Client("CLI012", "Free","39 rue des oiseaux","Niort"),
            new Client("CLI013", "Citroën","43 impasse des Marins","Roubais"),
            new Client("CLI014", "HP","24 tour des Vins","Strasbourg"),
            new Client("CLI015", "Yamaha","69 chemin des Vaisseliers","Limoges"),
            new Client("CLI016", "Royal Post","245 allée des Platanes","Nice"),
            new Client("CLI017", "Royal canin","25 tour des Parfums","Aix en Provence"),
            new Client("CLI018", "Rivoire & Carré","67 rue des Fruits","Orange"),
            new Client("CLI019", "Hays","18 allée des Arênes","Nîmes"),
            new Client("CLI020", "Total","11 passage Wilson","Evry"),
            new Client("CLI021", "Crédit Agricole","20 allée Jean-Moulin","Dijon"),
            new Client("CLI022", "Vinci","369 avenue Descartes","Nancy"),
            new Client("CLI023", "Bouygues","77 tour de Lyon","Bordeaux"),
            new Client("CLI024", "Alstom","66 passage sans issue","Toulouse"),
            new Client("CLI025", "Sanofi Aventis","55 chemin Paul Valery","Poitiers")
        ));
 
    public static Client getClient(String clientCode) {
        for (Client client : clients) {  
            if (client.getClientCode().equals(clientCode)) {  
                return client;  
            }  
        }
        return null;
    }
 
    public static List<Client> getClientsByName(String pieceOfName) {
        List<Client> clientsByName = new ArrayList<Client>();
        for(Client client : clients) {  
            if(client.getName().toLowerCase().startsWith(pieceOfName.toLowerCase()))  
                clientsByName.add(client);
        }
        return clientsByName;
    }
}
Code 7 : classe ClientModel.java de l'application demo_autocomplete.

Le descripteur glassfish-web.xml

Si comme moi, vous utilisez GlassFish pour déployer cette application de démonstration, vous devrez ajouter à votre projet le descripteur de déploiement glassfish-web.xml spécifique à GlassFish et le completer comme illustré ci-après pour ne plus avoir l'alerte PWC4011: Unable to set request character encoding to UTF-8 from context... dans le journal système du serveur.

En effet il semblerait, bien que JSF utilise habituellement UTF-8 pour restituer les vues, que le framework PrimeFaces demande au conteneur web de lui fournir le jeu de caractères à utiliser pour construire la vue à restituer. Or GlassFish lui indiquerait le jeu de caractères configuré par défaut, à savoir ISO-8859-1 et non pas UTF-8 !
Ce problème est notamment décrit à l'adresse https://code.google.com/p/primefaces/issues/detail?id=1195.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
    <parameter-encoding default-charset="UTF-8"/>
</glassfish-web-app>
Code 8 : descripteur de déploiement GlassFish glassfish-web.xml de l'application demo_autocomplete.

Autres possibilités du composant autoComplete

Le composant autoComplete de la librairie PrimeFaces intègre des fonctionnalités qui n'ont pas été illustrées dans l'application de démonstration.
Parmi elles, j'évoquerai en particulier les fonctionnalités suivantes :

  • Réglages du comportement.
    Les attributs ci-dessous méritent d'être pris en considération, notamment pour des questions de performance :
    • Délai avant requête (queryDelay): il s'agit du délai écoulé entre la frappe d'un caractère et l'émission de la requête AJAX. Par défaut, cette durée est fixée à 300 ms. Elle peut être étendue par exemple à 1 s pour réduire le nombre de requêtes AJAX émises vers le serveur web.
    • Nombre de caractères avant requête (minQueryLength) : nombre minimum de caractères que doit saisir l'utilisateur pour que le composant autocomplete propose des suggestions. Par défaut, les suggestions sont proposées dès la saisie du premier caractère.
    • Nombre de suggestions (maxResults) : nombre maximum de suggestions proposées. Par défaut, toutes les suggestions correspondant aux premiers caractères saisis sont affichées.
  • Affichage personnalisé des suggestions (balise <p:column/>) : les suggestions peuvent être présentées à l'affichage de manière plus élaborée qu'un simple libellé, par la présentation de données multiples en colonnes (voir l'image 4 ci-dessous).
  • Selection multiple (attribut multiple) : le composant autoComplete accepte également la selection de plusieurs suggestions comme illustré à l'image 4 ci-dessous.
    autocomplete multiple selectionImage 4 : composant autoComplete avec affichage personnalisé des suggestions et sélection multiple.
  • Liste déroulante (attribut dropdown) : le composant autoComplete peut être configuré pour se comporter comme une liste déroulante et afficher à la demande la liste des valeurs possibles (voir l'image ci-dessous).
    autocomplete comboboxImage 5 : liste déroulante autoComplete.
  • Info-bulles (balise <f:facet name="itemtip">) : une info-bulle peut être affichée au survol des suggestions pour fournir à l'utilisateur des précisions complémentaires (voir l'image ci-dessous).
    autocomplete itemtipImage 6 : composant autoComplete avec info-bulles sur les suggestions.
  • Affichage instantanée de la sélection (balise <p:ajax/>) : l'évènement de sélection d'une suggestion peut être intercepté pour émettre une requête AJAX à destination du managed bean sans avoir à attendre la validation explicite du formulaire par l'utilisateur.
Pour plus de détails sur la mise en oeuvre de ces fonctionnalités, veuillez vous reporter à la documentation des composants PrimeFaces disponible à l'adresse http://www.primefaces.org/documentation.html.

Le code source de l'application demo_autocomplete

Le code source de l'application web demo_autocomplete pré-configuré pour GlassFish et Ant peut être téléchargé sous la forme d'une archive ZIP en cliquant sur le lien ci-dessous :

Ce code source vous est mis à disposition uniquement à titre expérimental et à des fins éducatives. Vous restez par conséquent seul responsable de l'utilisation que vous en faites.