Archives Wild Code School
Java

JSF et Facelets - Part 2

Quête 40 sur 43

Wilder, tu es un dur! Tu veux te prendre la tête pousser JSF dans ses retranchements JSF ? OK cette quête est pour toi!

Objectifs

  • Aller plus loin avec Ajax pour JSF
  • Savoir créer des composants
  • Savoir utiliser des libs tierces
  • Dompter le cycle de vie d'une app JSF

Ajax ~ avancé !

Il y a plein de monde sur Github, c'est source d'erreurs quand tu veux traquer un nouveau Githuber. Dans la page track.jsf, tu voudrais pouvoir visualiser le Githuber avant de valider et de l'intégrer dans ta base de données.

Actuellement lorsque tu soumets le formulaire pour traquer un githuber, cela déclenche un appel sur l'API de GitHub afin de fabriquer un objet Githuber. Il va falloir le conserver au delà d'une reqûete, mais le garder en session n'est pas non-plus nécessaire. Le mieux est de rajouter une propriété Githuber gh (+getter/setter) à TrackController et de passer ce dernier en @ViewScoped...et du coup il va aussi falloir qu'il implements Serializable.

Attention là ça devient vraiment technique: TrackController dépend de GithubersService qui est lui-même @Dependent. Donc il va falloir qu'il implémente égalementSerializable pour être stocké exactement comme son parent... mais imaginons que tu préfères définir explicitement le cycle de vie de chacun de tes beans -ce que je te conseille- et que tu préfères annoter @RequestScoped GithubersService. Ca revient à demander à CDI d'injecter dans TrackController un bean qui a une durée de vie plus courte. Absurde!!!!!!

Tour de magie: ...sauf pour CDI, qui pour te satisfaire, va décider d'intercaler un "proxy" entre les deux classes. En gros, il "fabrique", au runtime, une fausse classe qui est assignable par GithubersService et qui a les mêmes signatues de méthodes (de fait, elle extends GithubersService). C'est le proxy. Et elle délègue ses appels de méthode à une vraie instance de GithubersService qu'elle garde en référence, et qui a bel et bien un scope plus court. Ainsi, d'une requête à l'autre, le proxy ne change pas, mais le GithubersService auquel il se réfère est renouvelé. Pour que le proxy puisse être instancié, il faut que la classe dont il dérive ait un constructeur vide:

java
@RequestScoped  
public class GithubersService {  
    private GithuberDAO dao;  
    private GithubClient ghClient;  

    public GithubersService() {  
        // for CDI  
    }  

    @Inject  
    public GithubersService(@Persisted GithuberDAO dao, GithubClient ghClient) {  
        this.dao = dao;  
        this.ghClient = ghClient;  
    }
...

Tant qu'à faire, autant écrire:

java
@RequestScoped  
public class GithubersService {  

    @Inject @Persisted  
    private GithuberDAO dao;  
    @Inject   
    private GithubClient ghClient;
    ...

sans aucun constructeur défini, le constructeur vide est implicite :) Et plus besoin d'implémenter Serializable.

Ensuite tu voudras contrôler l'état du TrackController :

  • login == null et gh == null ⇒ la page vient de s'initialiser, affichage du formulaire;

  • login != null et gh == null ⇒ tu viens de rentrer un login, il faut aller chercher le githuber sur l'API GitHub;

  • gh != null ⇒ tu as chargé ton githuber, affichage de la demande de confirmation

Ma foi le mieux est d'avoir une méthode toute simple du genre:

java
public boolean isValidating() {  
    return githuber != null;  
}

Ensuite, comme dans la première quête JSF, tu vas encadrer tout le formulaire de tracking (mais PAS le <h:messages>) par un <h:panelGroup> afin d'utiliser cette méthode:

xml
<h:panelGroup id="formPanel" rendered="#{!trackController.validating}">

Puis tu vas créer une seconde zone, elle encadrée par un panel avec la clause contraire!

xml
<h:panelGroup id="confirmPanel" rendered="#{trackController.validating}">

...dans laquelle tu vas mettre un message du genre <h2>Please confirm:</h2> et un <h:form> car tu vas devoir rajouer des boutons (des actions) pour confirmer le GitHuber ou pas.

Dans le form,

  • tu vas copier-coller à l'intérieur le code xml d'affichage d'un githuber, inscrit dans le <ui:repeat> de githubers.xhtml, sauf le bouton [untrack] bien sûr, en prenant soin de remplacer les #{githuber.xxx} par la référence de ton githuber à confirmer: #{trackController.githuber.xxx}

  • et tu vas aussi rajouter les deux boutons servant à la confirmation/infirmation:

xml
    <h:commandButton value="Yes" action="#{trackController.confirm}" styleClass="btn btn-primary" tabindex="0"/>  

    <h:commandButton value="No" action="#{trackController.cancel}" immediate="true" styleClass="btn btn-secondary">  
        <f:ajax render="..."/>  
    </h:commandButton>  

Tu l'as compris, il faut encore définir les méthodes confirm et cancel dans TrackController

  • confirm, tu avais déjà le code. Fais lui retourner "githubers?faces-redirect=true" pour aller voir directment le résultat après confirmation ET pour que l'URL dans la barre d'adresse du navigateur soit raffraîchie

  • cancel il suffit de réinitialiser tes propriétés (login = null; gh = null;)

A présent, quoi mettre à la place des ... ci dessus ? Tu peux essayer:

xml
        <f:ajax render="formPanel confirmPanel"/>

...mais ça ne marchera pas parce que JSF ne peut raffraîchir l'affichage d'un composant que...s'il est déjà affiché. LOL.

Si tu laisses ton code comme ça, au moment où tu soumets ton formulaire, confirmPanel n'est pas affiché du fait de rendered="#{trackController.validating}", aussi impossible pour JSF de le retrouver dans l'arbre de la page qu'il garde en mémoire. Il ne se raffichera jamais. Alors, la bonne pratique est d'encadrer l'ensemble (y compris le <h:messages/> par encore un autre panel qui lui, sera toujours affiché.

xml
<ui:define name="contents">  
    <h:panelGroup id="trackPanel">
        <h:messages/>
        <h:panelGroup id="formPanel" rendered="#{!trackController.validating}">
    ...

Ainsi il faudra indiquer:

xml
        <f:ajax render="trackPanel"/>

** Vérifie bien que cela fonctionne en appelant la page /faces/track.jsf**

↗
f:ajax javadoc
docs.oracle.com
↗
A comprehensive guide to JSF Ajax
beyondjava.net

Composition: Fabrique tes propres composants

Ca a dû te frapper que je t'avais demandé de dupliquer le code d'affichage d'un githuber dans track.xhtml. Mais il y a moyen d'en faire un composant réutilisable.

Tu vas créer un fichier /webapp/resources/wcs/githuber.xhtml et tu vas y mettre:

xml
<?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://xmlns.jcp.org/jsf/html"  
  xmlns:f="http://xmlns.jcp.org/jsf/core"  
  xmlns:ui="http://xmlns.jcp.org/jsf/facelets"  
  xmlns:composite="http://java.sun.com/jsf/composite"  
>  

<composite:interface>  
    <composite:attribute name="githuber" type="fr.wildcodeschool.githubtracker.model.Githuber" required="true"/>  
    <!-- already has attribute "rendered" by default http://java.net/jira/browse/JAVASERVERFACES-1894 -->  
</composite:interface>  

<composite:implementation>  
    <ui:fragment rendered="#{cc.attrs.rendered}">  
        <!-- your githuber html code here -->
    </ui:fragment>  
</composite:implementation>  
</html>

où à la place de #{githuber.xxx} tu vas écrire #{cc.attrs.githuber.xxx}

Enfin, il ne reste plus qu'à aller dans githubers.xhtml et track.xhtml, d'ajouter le namespace:

xml
xmlns:wcs="http://java.sun.com/jsf/composite/wcs"

et enfin de remplacer le code d'affichage d'un githuber par

xml
<wcs:githuber githuber="#{githuber}"/>

Tour de magie:

  • Le namespace wcs existe dès lors qu'il y a un composant jsf composite dans /webapp/resources/wcs/

  • Tout fichier xhtml composite dans ce répertoire wcs devient un tag.

  • composite:attribute définit un paramètre du composant, qui sera ensuite accessible en EL via le nom standard de la Map d'attributs du composant #{cc.attrs...}

Seul problème: dans track.xhtml on n'a pas besoin du bouton [untrack] et de toute manière il n'y a pas de composant dans la page s'appelant githubersList. Qu'à cela ne tienne, on n'a qu'à créer deux composite:attribute supplémentaires appelés

  • untrack de type java.lang.Boolean pour déclencher ou pas l'affichage du bouton

  • untrackRender de type java.lang.String pour savoir quel(s) composant(s) raffraîchir en cas d'action ajax.

Puis tu conditionnes le bouton [untrack] dans le composant composite:

xml
<h:form rendered="#{cc.attrs.untrack}">  
    <h:commandButton styleClass="btn btn-outline-danger btn-sm" value="Untrack">  
        <f:ajax listener="#{githubersController.untrack(githuber.id)}" render="#{cc.attrs.untrackRender}"/>  
    </h:commandButton>  
</h:form>

Vérifie que tes pages fonctionnent toujours

↗
Create a composite component @ Mykong
mkyong.com
↗
Composite specification
docs.oracle.com

Utiliser une lib d'extension à JSF

JSF a été concu pour être extensible. Il existe tout plein de bibliothèques de composants additionnels très pratiques:

Primefaces a le vent en poupe, intégrons la dernière version "community" à notre pom.xml

xml
<dependency>  
    <groupId>org.primefaces</groupId>  
    <artifactId>primefaces</artifactId>  
    <version>6.2</version>  
</dependency>

A partir de là on bénéficie directement de tous les composants grâce au namespace xmlns:p="http://primefaces.org/ui".

Tiens, si on utilisait leur carousel? Créée une nouvelle page carousel.xhtml en facelets et inspire-toi de leur live demo pour afficher la liste des githubers avec ce carousel, tout en utilisant le composant composite créé plus haut, bien-sûr :)

Ah, un coup classique avec les extensions JSF: celle-ci utilise jQuery. Or Bootstrap demande aussi d'inclure jQuery. Une version différente à tous les coups, donc conflit. Si tu ne veux pas devenir dingue, commente le jQuery en bas de ton template:

xml
<!-- jQuery is already imported by PrimeFaces -->
<!-- <script type="application/javascript" src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script> -->

↗
How to solve conflict with PF and jQuery
stackoverflow.com
↗
Facelets debugging

Consigne du travail à faire avec cette ressource

docs.oracle.com
↗
Primefaces manual
primefaces.org
↗
Primefaces getting started
primefaces.org
↗
Primefaces showcase
primefaces.org

Le cycle de vie et les Phaselistener

Imaginons que tu souhaites logger les accès à tes pages pour voir comment se comportent tes utilisateurs. Vu que des backing beans (les contrôleurs - le C de MVC) ne sont pas tous @RequestScoped, tu ne peux pas logger dans leur constructeur ou dans leur @PostConstruct, tu n'aurais pas forcément un log à chaque rechargement de page.

Autres possibilité : créer un bean @Named @ApplicationScoped avec la méthode adéquate, et l'appeler via une ViewAction.

  • <f:viewAction action="#{visitLogger.logViewAction}" onPostback="true"/> dans une section spécifique <f:metadata> Où la méthode attendue serait public String logViewAction() retournant null.

Comme c'est un mécanisme de JSF 2.2+, suivant ton projet tu risques de ne pas y avoir accès, donc tu peux te rabbatre sur un évènement de vue, là aussi dans la section <f:metadata>

  • <f:event type="preRenderView" listener="#{visitLogger.logPreRender}"/> Où la méthode attendue serait public void logPreRender(ComponentSystemEvent e).

Au passage tu peux mettre le type qui te convient le mieux. A titre de rappel voilà les phases JSF disponibles:

Toi qui te disais que tu avais de la chance d'utiliser des facelets et que tu pourrais définir ces balises dans ton template, je suis désolé de t'annoncer que ce n'est pas prévu comme ça. Regarde le lien StackOverflow dans les resources, tu es censé définir tes "sondes" dans chaque page. Pas terrible.

Si tu veux vraiment programmer cette fonctionnalité "orthogonalement", tu peux te brancher directment sur le cycle de vie du container JSF à l'aide d'un PhaseListener!

  • Un phase lisener se déclare dans faces-config.xml avec par exemple:
xml
<lifecycle>  
    <phase-listener>fr.wildcodeschool.githubtracker.util.VisitPhaseListener</phase-listener>  
</lifecycle>

Là aussi tu peux choisir la phase qui te convient le mieux. Personnellement je fais juste avant la "render-view", car on est alors sûr que la page demandée va être délivrée à l'utilisateur.

Implémente les trois solutions avc un log différent à chaque fois, navigue, agis sur les boutons en ajax, et constate dans quelle mesure chaque méthode est appelée.

↗
Lifecycle of a JSF application
docs.oracle.com
↗
PhaseListener specification
docs.oracle.com
↗
ViewAction vs Event components
oracle.com
↗
When to use PreRenderView vs ViewAction
stackoverflow.com
↗
How to call method on various lifecycle events
incepttechnologies.blogspot.fr
↗
When using composition termplating, where to put f:metadata
stackoverflow.com
↗
Michael Kurz on ViewActions
jsflive.wordpress.com
↗
Tag Event specification
javaee.github.io
↗
Tag ViewAction specification
javaee.github.io

Challenge

JSF dans tous ses états

  1. Aller au bout de la thématique Ajax
  2. Fabriquer son propre composant
  3. Utiliser une lib tierce
  4. Espionner sans vergogne ce que font les utilisateurs

Critères de validation

  • Tu as réussi à faire la page de tracking avec Ajax
  • Tu as créé un composant pour afficher un githuber
  • Tu as intégré le carousel de Primefaces
  • Tu as Implémenté des actions de vue et un phaselistener
  • Ton code est dispo dans une branche de ton projet github-tracker sur GitHub