ou comment créer un éditeur de formation de football
Depuis début 2011 je me suis mis à JSF2, si décrié par certain, et j’ai découvert Primefaces. J’ai rapidement accroché car tout simplement je l’ai trouvé plus joli que les cadors du marchés (RichFaces, IceFaces, ..) et puis aussi parce que les exemples du showcase de Primefaces m’ont beaucoup interessé puisqu’ils sont souvent basés sur des données liées au joueurs du FC Barcelone. Et vu que mon projet est lié au foot, j’ai de suite eu des idées sur comment mettre en oeuvre
J’utilise la version de développement de Primefaces (3.0-RC1-SNAPSHOT à ce jour) et la version finale (3.0) est prévue pour très bientôt. Mon environnement de développement est très standard : Netbeans 7, Glassfish 3.1.1, le tout sous linux. Coté base de données, après avoir tetsé du Derby j’utilise aussi MySql.
Rentrons dans le vif du sujet : le but est de créer un composant (bien que ce ne va pas être un composant JSF à proprement parler) qui permettra de définir la position de joueurs afin de définir un schéma de jeu (les fameux 4-4-2, 4-3-3 que connaissent les footeux).
Pour cela j’ai défini 2 entités que l’on va persister (je ne vais pas m’attarder sur la partie JPA2-EJB3 dans cette article) : La première; Formation, représente une formation (ou schéma tactique) :
@Entity | |
@Table(name = Formation.TABLE_NAME, | |
uniqueConstraints = { | |
@UniqueConstraint(columnNames = {Formation.COL_COD}) | |
}) | |
@Data | |
public class Formation implements Serializable { | |
private static final long serialVersionUID = 1L; | |
public static final String TABLE_NAME = "formation"; | |
public static final String COL_ID = "id_formation"; | |
@Id | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
@Column(name = COL_ID) | |
private Long idFormation; | |
public static final String COL_LIB = "lib_formation"; | |
@Basic(optional = false) | |
@NotEmpty(message = "Libelle obligatoire") | |
@Size(max = 30) | |
@Column(name = COL_LIB) | |
private String libFormation; | |
public static final String COL_COD = "cod_formation"; | |
@Basic(optional = false) | |
@NotEmpty(message = "Code obligatoire") | |
@Column(name = COL_COD) | |
@Size(max = 3) | |
private String codFormation; | |
@OneToMany(cascade = CascadeType.ALL, mappedBy = FormationItem.PROP_FORMATION) | |
@OrderBy(value=FormationItem.PROP_NUM) | |
private List<FormationItem> formationItemList; | |
} |
La deuxième entité, FormationItem, représentera les différents postes d’une formation :
@Entity | |
@Table(name = FormationItem.TABLE_NAME) | |
public class FormationItem implements Serializable { | |
private static final long serialVersionUID = 1L; | |
public static final String TABLE_NAME = "formation_item"; | |
public static final String COL_ID = "id_formation_item"; | |
@Id | |
@GeneratedValue(strategy = GenerationType.IDENTITY) | |
@Column(name = COL_ID) | |
private Long idFormationItem; | |
public static final String PROP_NUM = "numItem"; | |
public static final String COL_NUM = "num_item"; | |
@Basic(optional = false) | |
@NotNull(message = "Numero obligatoire") | |
@Column(name = COL_NUM) | |
private Integer numItem; | |
public static final String PROP_COORD = "coord"; | |
@Basic(optional = false) | |
@Column(name = PROP_COORD) | |
private Integer coord; | |
public static final String COL_ID_FORMATION = "id_formation"; | |
public static final String PROP_FORMATION = "formation"; | |
@ManyToOne(cascade= CascadeType.ALL) | |
@JoinColumn(name = COL_ID_FORMATION) | |
private Formation formation; | |
} |
Passons aux choses sérieuses coté JSF. L’idée va être de positionner les différentes FormationItem selon une grille, c’est à ça que sert la propriété FamFormation.coord, à représenter la case de la grille sur laquelle le FormationItem.numItem es positionné.
On ne va pas persister cette grille mais par contre on va avoir besoin d’un petit JSF Bean pour nous aider à faire cette représentation :
@Model | |
@Data | |
public class CanvasFormationItem { | |
private String strIdx; | |
private FormationItem formationItem; | |
} |
Ce bean va représenter chaque case de la grille que l’on va créer. Chaque case à un index et potentiellement un FormationItem.
Il ne reste plus qu’à créer un Controller et une page JSF.
Dans le controller on va avoir besoin de :
// Notre Grille : une liste de CanvasFormationItem | |
private List<CanvasFormationItem> lstTarget = new ArrayList<CanvasFormationItem>(); | |
// une Formation | |
private Formation current; |
Coté JSF, on va utiliser le composant DataGrid de PrimeFaces. Il permet d’afficher les données d’une ArrayList sur différentes colonnes, de paginer ou pas cette ArrayList, … bref, il va nous permettre d’afficher notre grille. Personnellement j’ai choisi une grille de 5*6 soit 30 cases. Mais on pourrait très bien affiner le positionnement des FormationItem en augmentant le nombre de cases. A l’affichage de notre page, il faut donc avoir construit une ArrayList de 30 CanvasFormationItem.
public void init(){ | |
// Mode création, on crée une nouvelle Formation | |
current = new Formation(); | |
// On initialise le nombre des FormationItem à positionner | |
// On est parti pour du foot à 11, mais on pourrait très bien faire du foot à 7 voire du rugby à XV | |
current.setFormationItemList(new ArrayList<FormationItem>()); | |
// par défaut on met numItem == coord | |
for (int i = 1; i <= 11; i++) { | |
FormationItem item = new FormationItem(); | |
item.setNumItem(i); | |
item.setCoord(i); | |
item.setFormation(current); | |
current.getFormationItemList().add(item); | |
} | |
// On créé la grille | |
lstTarget = new ArrayList<CanvasFormationItem>(); | |
for (int i = 1; i <= 30; i++) { | |
CanvasFormationItem item = new CanvasFormationItem(); | |
item.setStrIdx(String.format("%d", i)); | |
// On associe le FormationItem qui a le coord correspondant | |
if (current != null && current.getFormationItemList() != null) { | |
for (FormationItem fi : current.getFormationItemList()) { | |
if (fi.getCoord().equals(Integer.valueOf(i))) { | |
item.setFormationItem(fi); | |
break; | |
} | |
} | |
} | |
lstTarget.add(item); | |
} | |
} |
Basculons coté Web : On va avoir besoin d’une petite fonction javascript (toute petite et c’est la seule).
<script type="text/javascript"> | |
function handleDrop(event, ui) { | |
var droppedItem = ui.draggable; | |
droppedItem.fadeOut('fast'); | |
} | |
</script> |
D’abord un petit formulaire pour la Formation
<h:form> | |
<h:panelGrid columns="3"> | |
<h:outputLabel value="libellé" for="libFormation" /> | |
<p:inputText id="libFormation" | |
value="#{myController.current.libFormation}" | |
title="libellé" | |
> | |
<p:ajax event="blur" update="msg_libFormation"/> | |
</p:inputText> | |
<p:message id="msg_libFormation" for="libFormation"/> | |
<h:outputLabel value="Code" for="codFormation" /> | |
<p:inputText id="codFormation" | |
value="#{myController.current.codFormation}" | |
title="Code" | |
> | |
<p:ajax event="blur" update="msg_codFormation"/> | |
</p:inputText> | |
<p:message id="msg_codFormation" for="codFormation"/> | |
</h:panelGrid> |
et maintenant la fameuse DataGrid :
<!-- On met tout ca dans un panel avec en background une image de terrain de foot--> | |
<p:panel header="Field" styleClass="soccer_field"> | |
<!-- On créé la dataGrid pour afficher la grille--> | |
<p:dataGrid id="trgField" | |
value="#{myController.lstTarget}" | |
var="t" columns="5" | |
style="padding-top: 25px;margin-left: 20px;"> | |
<p:column> | |
<!-- Chaque case de la grille est un panel sans header d'une taille de 50*63--> | |
<p:panel id="trg" | |
style="width:50px;height:63px;opacity:0.5;"> | |
<!-- Si un FomationItem est associé à la case, on l'affiche | |
dans un panel sans header de 24*47 --> | |
<p:panel id="item" | |
rendered="#{!empty t.formationItem.numItem}" | |
style="width:24px;height:47px;"> | |
<h:outputText value="#{t.formationItem.numItem}" | |
style="text-align:center;font-weight: bolder;"/> | |
</p:panel> | |
<!-- On rend le panel du FormationItem draggable --> | |
<p:draggable for="item" revert="true" handle=".ui-panel-titlebar" stack=".ui-panel" /> | |
</p:panel> | |
<!-- On rend chaque case de la grille droppable en lui associant comme dataSource la DataGrid --> | |
<p:droppable for="trg" | |
tolerance="touch" | |
activeStyleClass="slotActive" | |
datasource="trgField" | |
onDrop="handleDrop"> | |
<!-- On ajoute un listener sur l'event onDrop --> | |
<p:ajax listener="#{myController.onDrop}" | |
update="@form"/> | |
</p:droppable> | |
</p:column> | |
</p:dataGrid> | |
</p:panel> |
On a rajouté un listener sur l’action onDrop, il faut donc le coder dans le Controller. On va gérer les modifications de coords sur les FormationItem dans ce listener. Puis en rafraichissant la DataGrid, ca sera automatiquement pris en compte.
public void onDrop(DragDropEvent event) { | |
// Puisque l'on a associé une datasource à nos éléments droppable | |
// alors event.getData() contient un objet Java et en l'occurrence puisque la datasource | |
// en question était la DataGrid, elle-même associée à lstTarget étant une ArrayList<CanvasFormationItem> | |
// on a donc accès à un objet de type CanvasFormationItem | |
// Attention, c'est l'objet draggé, donc l'objet source. | |
CanvasFormationItem item = (CanvasFormationItem) event.getData(); | |
// Pour avoir accès à l'objet où on a droppé, ce n'est pas encore possible | |
// du coup on feinte :D | |
// on recupère l'ID de l'élément DOM sur lequel on a droppé | |
String idCoord = event.getDropId(); | |
// et on récupère l'index de la grille (j'avoue c'est pas super classe) | |
idCoord = idCoord.substring(idCoord.indexOf("trgField")); // cf trgField dans la JSF | |
idCoord = idCoord.substring(idCoord.indexOf(':') + 1, idCoord.lastIndexOf(':')); | |
Integer coord = Integer.parseInt(idCoord) + 1; // les id autos commencent à 0 alors que les index de notre grille comment à 1 | |
// Maintenant on met à jour les coords des FormationItem et CanvasFormationItem incriminés | |
FamFormationItem fi = item.getFamFormationItem(); | |
for (CanvasFormationItem cfi : lstTarget) { | |
if (cfi.getStrIdx().equals(String.format("%d", coord))) { | |
// on swappe les formationItem | |
FormationItem fiTemp = cfi.getFormationItem(); | |
Integer oldCoord = null; | |
if (fi != null) { | |
oldCoord = fi.getCoord(); | |
} | |
cfi.setFormationItem(fi); | |
item.setFormationItem(fiTemp); | |
cfi.getFormationItem().setCoord(coord); | |
if (oldCoord != null && item.getFormationItem() != null) { | |
item.getFormationItem().setCoord(oldCoord); | |
} | |
break; | |
} | |
} | |
} |
Et voilà!