Afin de créer l’arbre des DOM nodes, l’arbre des tags est parcouru en profondeur. Un DOM node racine correspondant au tag racine et à un contexte racine correspondant au générique Engine est créé. Puis par défaut, pour chaque tag enfant, un DOM node enfant associé au tag enfant et au même contexte que le DOM node parent est créé. Ce comportement est toutefois modifié lorsqu’un metaBinding a été défini sur le tag enfant. Le métabinding précise quel(s) contexte(s) associer au tag enfant afin de créer un ou plusieurs DOM nodes enfants, en créant de nouveaux contextes si nécessaire.
Il existe deux sortes de métabindings : les forEach et les selects. Un seul métabinding peut être défini sur un tag donné. Un métaBinding peut servir à :
- Démultiplier un tag en l’associant à plusieurs contextes enfants du contexte courant (forEach).
- Afficher ou non un tag en fonction de conditions liées au contexte courant (select).
- Créer un unique contexte enfant du contexte courant en sélectionnant une classe générique.
Les deux premières utilisations sont similaires aux forEach et if des JSP. La troisième permet d’utiliser exactement la même classe Table pour afficher les instances de types différents.
Les métabindings sont usuellement définis à l’aide des annotations @ForEach, @ForEachContext, @Select, @SelectContext ou @DirectSelect.
@ForEAch
L’annotation @ForEach prend comme valeur une classe implémentant ObservableListExtractor, qui étend Function<Generic[], ObservableList>. Lors de la création de l’arbre des DOM nodes, la fonction sera appliquée aux génériques du contexte parent des DOM nodes à créer pour ce tag. Pour chaque élément de la liste obtenue, un nouveau contexte est créé à partir du contexte parent en ajoutant cet élément au début du tableau des génériques, et un DOM node associant ce contexte au tag est créé.
Des fonctions prédéfinies dans la classe ObservableListExtractor permettent de retourner diverses listes de génériques : instances d’un type, attributs d’un type (ceci inclut les relations), holders d’une instance correspondant à un attribut (ou relation) donné…
Exemple : Création d’une div pour chaque sous-instance d’un type :
1 2 3 |
@ForEach(ObservableListExtractor.SUBINSTANCES.class) public static class ContentRow extends FlexDiv { } |
Quand la Table est créée avec, par exemple, un @DirectSelect(Car.class), le HTML DOM node associé à la classe Table correspond à un contexte contenant les génériques {Car, Engine}. Des contextes {Audi S4, Car, Engine}, {BMW M3, Car, Engine}, etc. sont créés, ainsi qu’une div HTML pour chacun de ces contextes, cette div correspondant à une ligne du tableau obtenu.
@ForEachContext
L’annotation @ForEachContext est très similaire à @ForEach, la seule différence est qu’elle prend comme valeur une classe implémentant ObservableListExtractorFromContext, qui étend elle-même Function<Context, ObservableList>. Au lieu d’être appliquée au tableau de génériques du contexte parent, la fonction est appliquée directement au contexte. Cette annotation est utile lorsque l’on a besoin d’accéder à d’autres informations contenues dans le contexte que le tableau de génériques, comme par exemple des propriétés.
@Select
Elle prend en paramètre une classe implémentant ObservableValueSelector, qui étend
Function<Generic[], Generic>. Lors de la création du DOM node correspondant à ce tag, la fonction est appliquée aux génériques du contexte du DOM node parent de celui à créer. Si elle retourne null, aucun DOM node n’est créé. Sinon, un nouveau contexte est créé en ajoutant le générique retourné au contexte du nœud parent, et un DOM node associant ce DOM node et le tag est créé.
Cette annotation est équivalente au if que l’on peut trouver dans de nombreux langages de création de pages web.
Exemple :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@Select(path = { Header.class, CheckBoxDisplayer.class }, value = ObservableValueSelector.CHECK_BOX_DISPLAYER.class) @Select(path = { Header.class, HtmlSpan.class }, value = ObservableValueSelector.LABEL_DISPLAYER.class) @Children({ Content.class, Header.class }) @Children(path = Header.class, value = { CheckBoxDisplayer.class, HtmlSpan.class }) public static class ValueComponents extends Composite { } public static class CHECK_BOX_DISPLAYER implements ObservableValueSelector { @Override public Generic apply(Generic[] gs) { return Boolean.class.equals(gs[1].getInstanceValueClassConstraint()) ? gs[0] : null; } } public static class LABEL_DISPLAYER implements ObservableValueSelector { @Override public Generic apply(Generic[] gs) { return gs[1].getComponents().size() < 2 && !gs[1].isHidden() && ! Boolean.class.equals(gs[1].getInstanceValueClassConstraint()) ? gs[0] : null; } |
Le Header de ValueComponents contient à la fois un span et une checkbox, mais seul un des deux sera affiché suivant le type des données à afficher.
@SelectContext
Elle prend en paramètre une classe implémentant l’interface ObservableContextSelector, qui étend BiFunction<Context, Tag, ObservableValue>. Cette annotation fonctionne de façon très similaire à Select : si la valeur de l’ObservableValue est nulle, aucun DOM node n’est créé. Sinon, un DOM node est créé avec un contexte clonant le contexte retourné. Cela permet de créer ou non un nœud suivant des conditions dépendant du contexte, mais aussi de se placer dans un contexte sans aucun lien avec le contexte courant.
Exemple : L’éditeur affiche le générique correspondant au contexte contenu dans la sélection, qui doit être définie par ailleurs.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
@SelectContext(SELECTION_SELECTOR.class) public static class SelectionEditor extends TitledInstanceEditor { } public static class SELECTION_SELECTOR implements ObservableContextSelector { @Override public ObservableValue apply(Context context, Tag tag) { if (SelectionDefaults.class.isAssignableFrom(tag.getClass())) return ((SelectionDefaults) tag).getSelectionProperty(context); else throw new IllegalStateException("SELECTION_SELECTOR is applicable only to tags implementing SelectionDefaults."); } } |
@DirectSelect
Elle prend comme valeur une classe définissant un générique. Lors de la création du DOM node correspondant à ce tag, un contexte contenant le générique indiqué est créé.
Exemple : Création d’une table affichant les instances de Car :
1 2 |
@DirectSelect(Car.class) public class CarTable extends TitledInstancesTable { } |
1 2 3 4 |
@Children({ TitledInstancesTable.class, TitledInstancesTable.class }) @DirectSelect(path = TitledInstancesTable.class, value = { Bike.class, Car.class }) public static class GroupDiv extends FlexDiv { } |