JSF et Facelets - Part 2
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:
@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:
@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 == nulletgh == null⇒ la page vient de s'initialiser, affichage du formulaire; -
login != nulletgh == 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:
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:
<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!
<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>degithubers.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:
<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 -
cancelil 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:
<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é.
<ui:define name="contents">
<h:panelGroup id="trackPanel">
<h:messages/>
<h:panelGroup id="formPanel" rendered="#{!trackController.validating}">
...
Ainsi il faudra indiquer:
<f:ajax render="trackPanel"/>
** Vérifie bien que cela fonctionne en appelant la page /faces/track.jsf**
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 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:
xmlns:wcs="http://java.sun.com/jsf/composite/wcs"
et enfin de remplacer le code d'affichage d'un githuber par
<wcs:githuber githuber="#{githuber}"/>
Tour de magie:
-
Le namespace
wcsexiste dès lors qu'il y a un composant jsf composite dans/webapp/resources/wcs/ -
Tout fichier xhtml composite dans ce répertoire
wcsdevient un tag. -
composite:attributedé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
-
untrackde typejava.lang.Booleanpour déclencher ou pas l'affichage du bouton -
untrackRenderde typejava.lang.Stringpour savoir quel(s) composant(s) raffraîchir en cas d'action ajax.
Puis tu conditionnes le bouton [untrack] dans le composant composite:
<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
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:
-
RichFaces (JBoss, abandonné)

Primefaces a le vent en poupe, intégrons la dernière version "community" à notre pom.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:
<!-- 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> -->
Consigne du travail à faire avec cette ressource
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 seraitpublic String logViewAction()retournantnull.
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 seraitpublic 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.xmlavec par exemple:
<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.

Challenge
JSF dans tous ses états
- Aller au bout de la thématique Ajax
- Fabriquer son propre composant
- Utiliser une lib tierce
- 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
