Composant JavaScript et composant ZK


ZK est un framework Web qui permet de produire des applications web simplement et entièrement en Java (malgré toute ressemblance, Zenika n'est pas à l'origine de ZK). Comme JSF ou Wicket, il s'agit d'un framework orienté composants, dans lequel des composants côté serveur, écrits en Java, pilotent des composants côté navigateur, écrits en JavaScript.

Nous souhaitons utiliser une librairie JavaScript existante (Google Chart en l'occurrence) dans notre application ZK. Cet article explique comment développer son propre composant ZK à partir d'un composant JavaScript sur étagère.

Démarrage du projet

ZK propose des archetypes Maven pour démarrer rapidement un projet, il suffit d'une ligne de commande pour commencer:

mvn archetype:generate -DarchetypeCatalog=http://mavensync.zkoss.org/maven2/
    -DarchetypeGroupId=org.zkoss -DarchetypeArtifactId=zk-archetype-component
    -DgroupId=com.zenika.zk -DartifactId=gchartz -Dversion=1.0-SNAPSHOT
    -Dpackage=com.zenika.zk.gchartz -Dcomponent-class=GChart -Dcomponent-name=gchart
    -Dzk-version-since=6.5

On obtient alors un embryon de composant Java (GChart.java), un embryon de composant JavaScript (GChart.js), et des fichiers de configuration pour faire le lien entre les deux (zk.wpd et lang-addon.xml). Le pom.xml contient un plugin yuicompressor-maven-plugin-zk chargé d'assembler les fichiers JavaScript en un module et de le "minifier" façon JSMin. Enfin, le module test contient une application Web pour tester notre composant, on pourra facilement la lancer en utilisant le plugin Jetty préconfiguré dans le pom.xml. Bref, pour voir apparaître notre premier composant, il suffit de faire: Composant ZK Etape 1

mvn jetty:run

Chargement de la librairie JavaScript

Si la librairie JavaScript à envelopper se présente sous la forme d'un fichier .js, c'est très simple, une directive script dans la vue et le tour est joué.

<?script type="text/javascript" src="/js/gchart.js"?>

Mais en procédant ainsi, toutes les applications mettant en oeuvre le composant seraient contraintes d'embarquer par elles-mêmes le fichier gchart.js. Pour rendre notre composant réutilisable, on peut joindre la librairie avec le composant. Pour demander à ZK de la charger, on ajoute une ligne dans le fichier zk.wpd (Web Package Descriptor):

<script src="gchart.js" />

Certains préféreront charger la librairie depuis un CDN (Content Distribution Network); Pour la librairie Google Chart on n'a guère le choix en fait, il faut la récupérer dynamiquement sur les serveurs de Google. On peut se servir de jQuery (jq est l'alias du jQuery fourni par ZK) pour obtenir un script, comme le chargement se fait alors de manière asynchrone, il ne faut pas oublier retarder l'utilisation de la librairie après son chargement au moyen d'une callback:

jq.getScript("http://host/gchart.js", function() {
    // Load Callback
});

Certaines librairies, comme Dojo ou Google Chart (encore!), utilisent AMD (Asynchronous Module Definition) pour découper le code JavaScript en modules et paralléliser les téléchargements. Il faut, ici aussi, penser à retarder l'utilisation de la librairie après le chargement du module:

// Chargement Google Loader
jq.getScript("https://www.google.com/jsapi",function() {
    var googleLoader=window.google;
 
    // Chargement Google Chart
    googleLoader.load("visualization", "1", {
        packages:["corechart"],
        callback:function() {
            var googleChart=window.google.visualization;
 
            // Utilisation Google Chart
            var chart = new googleChart.LineChart(document.getElementById('chart_div'));
            chart.draw(data, options);
        }
    });
});

Etant donnée que ZK possède son propre système de chargement modulaire, on ne peut pas servir de l'événement jQuery load ou de la fonction Google setOnLoadCallback.

Naissance d'un composant ZK

Cycle de vie composant ZK A la réception de la première requête HTTP, ZK va charger la vue, généralement sous forme de fichier ZUL, qui contient notre composant sous la forme d'une balise <gchart> (DemoWindowComposer n'est que le contrôleur de cette vue):

<zk>
    <window title="Google Chart" border="normal" width="640px" height="480px"
        apply="com.zenika.zk.gchart.DemoWindowComposer">
        <gchart id="gChart" title="Le titre"/>
    </window>
</zk>

A partir de ce fichier XML, ZK va instancier notre composant Java, le configurer, puis le placer dans l'arbre de composants, et enfin ordonner au navigateur de faire de même avec le composant JavaScript correspondant. L'association balise XML/ classe Java/classe JavaScript est décrite dans un fichier lang-addon.xml

<component>
    <!-- Balise XML -->
    <component-name>gchart</component-name>
    <!-- Classe Java -->
    <component-class>com.zenika.zk.gchart.GChart</component-class>
    <!-- Classe JavaScript -->
    <widget-class>gchartz.GChart</widget-class>
</component>

Côté Java la classe GChart étend XulElement, et contient juste quelques propriétés. La seule méthode notable est renderProperties() destinée à sérialiser ces propriétés pour les envoyer au navigateur sous forme de JSON:

private String _title;
public String getTitle() { return _title; }
public void setTitle(String title) { _title=title }
protected void renderProperties(ContentRenderer renderer)
        throws IOException {
    super.renderProperties(renderer);
    render(renderer, "title", _title);
}

Les valeurs, comme _title ci-dessus, doivent être convertible en JSON. Pour cela, ZK s'inspire beaucoup de la librairie JSON Simple, sont donc autorisés:

  • Les types Java standard: Integer, Long, Float, Boolean, String, etc.
  • Toute classe qui implémente JSONAware et définit une méthode toJSONString()
  • Deux types composites qui imitent JavaScript: JSONObject (= Map) et JSONArray (= List)

Côté JavaScript, on retrouve les mêmes propriétés que côté serveur, le $define automatise la génération des getter/setters. Une fonction redraw (ou bien un moule/mold) génère le HTML: un <div> avec un id spécifique est ajouté au DOM. Puis une fonction bind_ permet d'activer l'écoute d'événements sur le DOM; Nous nous en servirons pour initialiser le composant Google en récupérant, grâce à la fonction $n(), le <div> précédemment créé:

_title:'',
$define: {
    title: null
},
redraw:function(out) {
    out.push('&lt;div ', this.domAttrs_(), '&gt;');
    out.push('Google Chart "',this._title,'" loading...');
    out.push('</div>');
},
bind_: function () {
    var self=this;
    this.$supers(gchartz.GChart,'bind_', arguments);
    requireGoogleChart(function(googleChart) {
        var data = ..., options = { title: self._title };
        self._gChart=new googleChart.LineChart(self.$n());
        self._gChart.draw(data, options);
    });
},

Composant ZK Etape 2 Une fois le Google Chart instancié, on garde sa référence ( _gChart) pour pouvoir le mettre à jour par la suite. En initialisant les variables data et options de la même manière que title, on arrive rapidement au résultat attendu:

Mettre à jour le composant

Une fois le graphique affiché, on va vouloir le modifier, changer le titre ou les valeurs affichées parce que l'utilisateur aura cliqué sur un bouton. Par exemple, pour changer le titre du graphique, le contrôleur de la page appellera la méthode setTitle du composant. Celle-ci déclenchera alors un smartUpdate, qui provoquera la mise à jour de l'attribut côté JavaScript. La méthode smartUpdate fonctionne de manière similaire à la méthode render utilisée pendant l'initialisation.

public void setTitle(String title) {
    if (!Objects.equals(_title, title)) {
        _title = title;
        smartUpdate("title", _title);
    }
}

Lorsque la fonction setTitle du composant JavaScript sera invoquée, on pourra en profiter mettre à jour le graphique:

$define: {
    title: function() {
        if(this._gChart) {
            // Mettre à jour le Google Chart
        }
    }
}

Sur le même principe, si on souhaite appeler la fonction updateChart sur le composant JavaScript, on écrira dans le composant Java:

public void updateChart() {
    Clients.response(new AuInvoke(this, "updateChart"));
}

Pour résumer, smartUpdate et Clients.response permettent au composant Java de donner des ordres au composant JavaScript et de le télécommander à distance.

Remontée des événements

Jusqu'ici nous avons vu la descente des ordres de modification des composants Java vers JavaScript, à présent nous allons voir la remontée des événements. On s'abonne aux événements émis par le Google Chart immédiatement après l'avoir créé dans la fonction bind_. Lorsqu'un événement survient la fonction _onSelect sera appelée:

self._gChart=new googleChart.LineChart(self.$n());
        googleChart.events.addListener(self._gChart, 'select',
            function() {
                self._onSelect()
            }
        );

Dans la fonction _onSelect, on prépare un nouvelle événement à destination du serveur. Cet événement est émis avec la fonction fire qui déclenchera un requête Ajax.

ZK est néanmoins assez malin, car si aucun listener n'a été enregistré coté serveur, la fonction fire sera sans effet, le composant restera muet puisque personne ne l'écoute.
_onSelect:function() {
    var selectedItems = this._gChart.getSelection(),
        eventData;
    if (selectedItems && selectedItems.length>0) {
        eventData={
            row:selectedItems[0].row,
            column:selectedItems[0].column
        }
        this.fire("onSelect", eventData);
    }
},

Côté serveur, la méthode service du composant Java accueille la requête Ajax brute, et permet de la traiter: Parsing JSON de l'événement JavaScript, puis émission d'un événement Java dans la file événementielle à destination du contrôleur de la page.

static {
    addClientEvent(GChart.class, "onSelect", 0);
}
 
public void service(AuRequest request, boolean everError) {
    if (request.getCommand().equals("onSelect")) {
        // Parsing JSON
        Integer row=   (Integer) request.getData().get("row");
        Integer column=(Integer) request.getData().get("column");
        // Emission d'un événement onSelect
        SelectEvent event=new SelectEvent("onSelect", this, row, column);
        Events.postEvent(event);
    } else {
        super.service(request, everError);
    }
}

Pour conclure, si savoir développer un composant n'est pas vital pour réaliser une application web avec ZK, ce n'est pas très compliqué et assez bien cadré par le framework. Cela permet en outre, de comprendre le fonctionnement interne du moteur Ajax qui synchronise une classe Java côté serveur avec une classe JavaScript dans le navigateur.


Fil des commentaires de ce billet

Ajouter un commentaire

Le code HTML est affiché comme du texte et les adresses web sont automatiquement transformées.