JavaScript – Java & Moi https://javaetmoi.com Développeur Java, Spring & co, et fier de l'être Sun, 21 Apr 2024 17:49:09 +0000 fr-FR hourly 1 https://wordpress.org/?v=6.9.4 https://javaetmoi.com/wp-content/uploads/2022/05/cropped-java-icon-32x32.png JavaScript – Java & Moi https://javaetmoi.com 32 32 Formulaire dynamique en Vue.Js https://javaetmoi.com/2017/05/formulaire-dynamique-en-vue-js/ https://javaetmoi.com/2017/05/formulaire-dynamique-en-vue-js/#comments Thu, 18 May 2017 16:15:36 +0000 http://javaetmoi.com/?p=1726 Continuer la lecture de Formulaire dynamique en Vue.Js ]]> Dans ce billet, nous allons mettre en pratique l’initiation à Vue.js reçue le mois dernier. Je vous propose de coder un pseudo Google Form avec l’aide de Vue.js, de Bootsrap et du framework de validation VeeValidate.
Le formulaire HTML est généré automatiquement à partir d’un paramétrage JSON récupéré par une API REST. Nous n’aborderons pas ici la partie serveur.
Un utilisateur peut sauvegarder son formulaire à l’état de brouillon afin de poursuivre ultérieurement sa saisie. Le formulaire à afficher peut donc être pré-saisi.
La validation est dynamique : elle se fait au fur et à mesure de la saisie du formulaire.
Voici un exemple de formulaire :

Démo live

Avant de passer aux explications, mettons en action ce formulaire. HTML, code JavaScript et rendu graphique sont accessibles dans ce snippet JSFiddle codé avec Vue 2.2, VeeValidate 2.0 et Bootstrap 3.3 :

Le modèle objet du formulaire

Vue.js implémentant le pattern MVC, intéressons-nous au modèle objet sous-jacent à notre formulaire :

  • Un formulaire est composé d’une liste de questions.
  • Chaque question comporte un libellé suivi d’un champ de saisi.
  • Le champ de saisi peut différer en fonction du type question : zone de saisie sur une ligne, radio bouton, liste déroulante, zone de texte multi-lignes …

Le paramétrage du formulaire (et son état courant) est décrit sous forme d’un tableau de questions en notation littérale JavaScript :

var formParameters =
    [ { id: 1, label: 'First Name', type: 'input', answer: 'Antoine' },
      { id: 2, label: 'Last Name', type: 'input' },
      { id: 3, label: 'Email', type: 'input'},
      { id: 4, label: 'Job', type: 'select', options: ['...', 'Developer', 'Ops', 'Project Manager'], answer: 'Developer' },
      { id: 5, label: 'Gender', type: 'radio', options: ['Male', 'Female'], answer: 'Male'},
      { id: 6, label: 'Address', type: 'textarea', placeholder: 'Your zip code and city'}
    ];

En pratique, le paramétrage du formulaire sera récupéré par API REST au chargement de la page. Afin de rendre autonome notre exemple, il y est hard-codé.
Voici à quoi ressemble le point d’entrée de notre application Vue.js :

var app = new Vue({
        el: '#dynform',
        data: {
            questions: []
        },
        created: function () {
            // Dynamic Form could be load from a REST API
            this.questions.push(formParameters);
        }
    });

Le tableau de questions (notre modèle) est stocké dans l’objet data de l’instance Vue.

Arbre de composants

Orienté composants, Vue.js permet de structurer la génération du formulaire à l’aide de plusieurs composants.
Le composant générique <form-question> est responsable d’afficher le libellé de la question puis de sélectionner le sous-composant approprié pour la zone de saisie. Exemple : un <form-radio> lorsque la question est de type radio. Il gère également l’affichage du caractère wildcard * lorsque la question est obligatoire.

La page HTML

La majorité du code HTML est localisé dans les templates Vue.js des sous-composants. Le code HTML de la page du formulaire est réduit à un simple <form> générant autant de balises <form-question> que de questions paramétrées dans le modèle du formulaire :

<form id="dynform" class="panel-body form-horizontal" v-on:submit.prevent="displayForm">
    <div class="row">
        <div class="col-md-12 ">
            <form-question v-for="question in questions" :question="question"
                           :key="question.id"></form-question>
        </div>
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
</form>

La soumission du formulaire déclenche la fonction displayForm dont nous verrons l’implémentation par la suite.

Le composant <form-question>

Le composant <form-question> accepte en paramètre l’une des questions de notre formulaire.
Conditionnel, son template est généré dynamiquement en JavaScript par concaténation de String. Ne pouvant utiliser la propriété template, il est initialisé dans la fonction created au travers de la propriété this.$options.template.

Vue.component('form-question', {
    props: ['question'],
    created: function () {
        this.$options.template = '<div class="form-group"> ' +
            '<label :for="question.id" class="col-sm-3 col-lg-2 control-label">' +
            '{{question.label}}';
        if (this.question.required || ((this.question.validate !== undefined) && this.question.validate.match("required"))) {
            this.$options.template += '<em>*</em>'
        }
        this.$options.template += '</label>' +
            '<div class="col-sm-9 col-lg-10">';
        switch (this.question.type) {
            case 'input'  :
                this.$options.template += '<form-input :question="question"></form-input>';
                break;
            case 'select' :
                this.$options.template += '<form-select :question="question"></form-select>';
                break;
            ...   
        }
        this.$options.template +=
            '</div>' +
            '</div>';
    }
});

Le switch case permet de sélectionner le sous-composant Vue à afficher : <form-input>, <form-select>, <form-radio> et <form-textarea>. Chacun d’eux accepte un seul paramètre : la question courante à afficher.

Form Input

Au travers du <form-input>, regardons de plus près à quoi ressemble un sous-composant. Voici une version dénudée de validation :

Vue.component('form-input', {
    props: ['question'],
    template: '<div class="form-group">' +
    '<input :name="question.label" :id="question.id" type="text" class="form-control"' +
    '       v-model="question.answer"  :placeholder="question.placeholder"/>' +
    '</div>'
});

Les attributs id, name et placeholder sont attribués par binding en utilisant la syntaxe raccourcie de v-bind:name= »question.label ».
La valeur du champs de saisie référence le modèle question.answer.

Les autres sous-composants sont conçus sur le même modèle.

Validation du formulaire

La validation du formulaire est implémentée à l’aide de la librairie VeeValidate.
Chaque question du modèle se voit ajouter un attribut validate spécifiant les contraintes de validation à l’aide de la syntaxe VeeValidate.
Exemple sur le nom de famille qui est requis, ne doit comporter que des caractères alphabétiques et au minimum 2 caractères :

{id: 2, label: 'Last Name', type: 'input', validate: "required|alpha|min:2"}

Le template de chaque sous-composant est agrémenté avec un attribut v-validate bindé sur le modèle validate. En cas d’erreur de validation, le message d’erreur est affiché dans un <span> et la classe CSS has-error de Bootstrap et ajouté au <div> englobant de type form-group.
Complétons ainsi notre exemple du sous-composant <form-input> :

Vue.component('form-input', {
    props: ['question'],
    template: '<div class="form-group" :class="{\'input\': true, \'has-error\': errors.has(question.label) }">' +
    '<input type="text" v-validate="question.validate" :id="question.id" :name="question.label"'+
    'class="form-control" v-model="question.answer":placeholder="question.placeholder"/>' +
    '    <span v-show="errors.has(question.label)" class="help-block">{{ errors.first(question.label) }}</span>' +
    '</div>'
});

Factorisation du template de gestion des erreurs

La gestion des erreurs de validation est identique sur chaque sous-composant.
Le <div class= »form-group »> se voit ajouter la classe CSS Boostrap has-error lorsque VeeValidate détecte une ou plusieurs erreurs.
Le <span> affiche le 1er message d’erreur détecté.
Ayant toutes 2 besoins d’accéder à la propriété errors locale au sous-composant, ces balises HTML ne peuvent être remontées dans le composant <form-question>.
Pour éviter la duplication de code HTML dans les template, il est néanmoins possible de factoriser le code dans une fonction questionTemplate :

function questionTemplate(customField) {
    return '<div class="form-group" :class="{\'input\': true, \'has-error\': errors.has(question.label) }">' +
        customField +
        '<span v-show="errors.has(question.label)" class="help-block">{{ errors.first(question.label) }}</span>' +
        '</div>'
}

Vue.component('form-input', {
    props: ['question'],
    template: questionTemplate('<input v-validate="question.validate" :name="question.label" :id="question.id" type="text" class="form-control" v-model="question.answer" :placeholder="question.placeholder"/>')
});

A noter que cette factorisation n’a pas été mise en œuvre dans le snippet JSFiddle.

 

Validation globale

Avant de soumettre au serveur le formulaire, une validation globale est réalisée côté client.
En cas de succès, le snippet affiche au format JSON les données à transmettre. En cas d’erreur, il affiche leur nombre et les messages d’erreur à côté de chaque champ en erreur.

La validation d’un formulaire composé de plusieurs sous-composants n’est pas native avec VeeValidate, preuve en est l’issue Can’t validate form with multiple child components. Plutôt que de passer par un composant faisant office de bus de messages, j’ai choisi d’utiliser l’API de validation.
L’instance $validator de l’application Vue est recyclée. Les contraintes de validation de chaque champ lui sont rattachées (méthode attach). L’objet data référence les données du formulaire à valider. Cet objet est passé à la méthode de validation validateAll qui accepte 2 fonctions de callback :

  1. En cas de succès (méthode then), un tableau contenant les données à soumettre au serveur est construit puis, dans le cadre de la démo, affiché simplement dans une popup.
  2. Lorsqu’un ou plusieurs champs sont invalides (méthode catch), un artifice consistant à itérer sur l’ensemble des sous-composants et à déclencher leur validation individuelle permet d’afficher le message d’erreur local et d’activer le style CSS approprié. Le nombre de champs invalide est affiché dans une popup.
methods: {
    displayForm: function(event) {
        var $this = this;
        var $validator = this.$validator;
        var data = {};
        this.questions.forEach(function(question) {
            if (question.validate !== undefined) {
                $validator.attach(question.label, question.validate);
                data[question.label] = question.answer;
            }
        });
        var $questions = this.questions;
        $validator.validateAll(data).then(function() {
            var form = [];
            $questions.forEach(function(question) {
                form.push({
                    id: question.id,
                    label: question.label,
                    answer: question.answer
                });
            });
            alert("Valid form: "+JSON.stringify(form));
        }).catch(function(error) {
            $this.$children.forEach(function(child) {
                child.$children.forEach(function(child) {
                    child.$validator.validateAll().then(function() {}).catch(function() {});

                });
            });
            alert("Invalid form. Error count:  " + $validator.getErrors().count());
        })
    }
}

Conclusion

En une centaine de lignes de code JavaScript, nous disposons d’une application web capable d’afficher n’importe quel formulaire décrit en JSON.
Pour l’instant limité, le nombre de champs de saisie ne demande qu’à être étendu : sélection multiple, date avec calendrier, upload de fichiers …

Pour des questions de sécurité et d’intégrité des données, la validation effectuée côté client devra être redondée côté serveur.

]]>
https://javaetmoi.com/2017/05/formulaire-dynamique-en-vue-js/feed/ 3
Codez le lab Vue.js de Devoxx France 2017 https://javaetmoi.com/2017/04/codez-lab-vue-js-devoxx-france-2017/ https://javaetmoi.com/2017/04/codez-lab-vue-js-devoxx-france-2017/#respond Sun, 09 Apr 2017 18:16:16 +0000 http://javaetmoi.com/?p=1677 Continuer la lecture de Codez le lab Vue.js de Devoxx France 2017 ]]> Au cours des précédentes éditions de Devoxx France, je me suis familiarisé avec les frameworks JavaScript du moment : AngularJS en 2013 puis Angular 2 et ReactJS en 2016. Cette année, ce fut au tour d’un nouveau venu, à savoir Vue.js. Je l’ai testé au travers du Hands-on Lab animé par Emmanuel Demey et Aurélien Loyer. Si vous n’avez pas eu la chance d’y participer, cet article a pour humble objectif de vous aider à réaliser ce Lab par vous-même, tel un tutoriel. Il complète le code disponible sur le dépôt GitHub du Lab ainsi que les slides consultables en ligne. Vous pouvez également l’utiliser pour étudier à quoi ressemble une application Vue.js et découvrir ses principaux concepts.

Emmanuel et Aurélien sont consultants web chez Zenika Lille. Familiarisés avec Angular, ils ont découvert VueJS au travers d’un projet personnel.
VueJS nous est présenté comme une librairie (et non un framework) dédiée à la création d’interfaces web HTML. Il se veut simple et efficace, idéal pour créer rapidement une application web. Ses concepts principaux sont les Vues (il ne s’appelle pas Vue.js pour rien), les Directives, les Composants et le Binding. Tels les Web Components, Vue.js utilise le Shadow DOM pour scoper le style CSS des composants.
Après cette courte introduction, place au CodeLab.

L’application Zenika Ecommerce

Au cours de ce CodelLab, vous allez développer une petite application de e-commerce dédiée à la vente de bières (nos speakers ne sont pas Lillois pour rien).

La page est décomposée en 2 parties :

  1. un menu supérieur permettant d’accéder au panier et donnant quelques informations sur le contenu de ce dernier,
  2. une liste de bières en stock que vous pouvez acheter.

Le template HTML et les ressources statiques de cette page nous sont fournies dans la branche step0. L’objectif du Lab sera de les dynamiser en les intégrant dans des composants Vue.js.
Les données (à savoir les bières) seront tout d’abord hard-codées à la main dans le JS avant d’être récupérées d’un serveur Node.JS via une API REST.

Ce Lab est développé en ECMAScript 6 (alias JavaScript 2015). L’utilisation de la syntaxe raccourcie de déclaration de méthodes est encouragée.

Pré-requis

Les instructions des différents exercices du Lab sont données dans le fichier index.md. Chaque exercice est reconnaissable au pattern PW<Numéro> (pour Project Work ?).

Avant de commencer à implémenter un exercice, vous devrez tout d’abord vous référer à la partie théorique des slides.
Avant d’aller plus loin, les pré-requis suivants sont nécessaires :

L’installation du cli Vue et le boostrap de l’application feront partis de l’exercice PW1.

PW0 – Ressources mises à dispositions

Cette étape se résume à cloner le repo https://github.com/Gillespie59/devoxx-vuejs et à checkouter la branche step0.

Voici les fichiers / répertoires qui vous intéresseront :

  • docs/index.md : instructions permettant de réaliser le Lab
  • server/ : serveur Express / Node.JS utilisé à partir de l’exercice 4 pour exposer la liste de bières sous forme d’API REST. A noter que le fichier server/beers.json sera utilisé dès l’étape 2 pour hard-codé sous forme d’objet JavaScript le tableau de bières à afficher.
  • static/ : ressources statiques (CSS, fonts et images)
  • html : template statique HTML de la page d’accueil utilisé dès l’étape 2.
  • html : non utilisé lors du lab par faute de temps, ce template permet d’initier la page affichant le panier utilisateur (PW7)

PW1 – Application blanche Vue.JS

Avant de créer votre première application, un peu de théorie est nécessaire pour vous familiariser avec les principaux concepts de Vue.js. Pour se faire, parcourez les 5 slides suivants :

Lors de la conception du binding de Vue.js, son concepteur n’a pas souhaité utiliser le préfixe data– afin d’être conforme au W3C. Son point de vue est que le code généré par Vue.js est lui conforme W3C.

Pour créer une application Vue.JS, à l’instar de ember-cli et angular-cli, on peut utiliser l’interface en ligne de commande (command-line interface) vue-cli. L’utilitaire vue-cli propose différents types de squelette : du plus basique à celui basé sur webpack. Dans le Lab, nous utiliserons le squelette webpack.

A la fin du PW1, la d’accueil du template Vue.js doit s’ouvrir dans votre navigateur :

Le code source de l’application que vous allez compléter au cours du Lab se trouve dans le répertoire src/.
Le fichier main.js est le point d’entrée de l’application. On y retrouve la déclaration de la vue racine de l’application :

new Vue({
   el: '#app',
   template: '<App/>',
   components: { App }
 })

L’objet Vue est l’objet principal de la librairie. Son constructeur prend en paramètre un objet JS dont les propriétés sont normalisées. Ici, notre vue racine en définie 3 :

  • el : associe la vue avec un élément du DOM ayant l’identifiant app
  • template : l’élément du DOM sera remplacé par le code HTML du template, ici une balise personnalisée <App/>
  • components : composants Vue.js nécessaires au rendu de la vue. Ici, le composant App est référencé. C’est lui qui va être chargé d’interpréter la balise <App/>

Dans le fichier index.html, nous retrouvons le <div> portant l’identifiant « app » et qui sera donc associé à la vue racine :

<body>
  <div id="app"></div>
</body>

Particulièrement simple, cette vue racine ne comporte ni données ni gestionnaire d’évènements.

Le code source du composant App est localisé dans le fichier src/App.vue. L’approche composant de Vue.js s’inspire très fortement du standard Web Components dont Polymer est une implémentation. L’objectif d’un composant est d’encapsuler du code (HTML + JS + CSS) afin de pouvoir le réutiliser. Un composant est associé à une balise HMTL. Ici, à la balise <App/>. Vous l’aurez remarqué, c’est le nom du fichier .vue qui détermine le nom de la balise HTML associée.
Un composant peut être déclaré par programmation via la méthode Vue.component() ou bien décrit dans un fichier dédié portant l’extension .vue. Le fichier App.vue est scindé en 3 parties :

  1. <template> : code HTML templatisé à l’aide de la syntaxe Mustache.
  2. <scripts> : code JavaScript du composant : nom, données, comportement, méthode callback appelée lors des différentes étapes du cycle de vie du composant …
  3. <style> : style CSS global ou spécifique au composant. Pour un style spécifique, il faut ajouter l’attribut scoped. Le navigateur utilise alors le Shadow DOM.

Des loaders Webpack sont chargés de transformer le contenu des fichiers .vue en JavaScript.
Dans la suite du Lab, vous aurez à personnaliser le fichier App.vue.

PW2 – Dynamiser la page à l’aide d’un composant

Cet exercice est sans nul doute le plus difficile du Lab. Vous allez en effet devoir manipuler pour la 1ière fois les principales notions de Vue.js : les composants, le binding et le templating.
Avant de commencer l’énoncé, veuillez prendre connaissance des slides théoriques sur les composants :

  • Global Components 3-1 à 3-6
  • Local component 3-7
  • Fichiers .vue 3-7 et 3-8

Le mécanisme de communication entre composants est similaire à celui d’Angular 2 : le composant parent passe des propriétés à ses composant enfants. Les enfants émettent des évènements à leur parent.

Solution

Commencer par créer 2 fichiers Menu.vue et Beer.vue dans le sous-répertoire components.
Les 2 composants Beer et Menu sont référencés dans la vue racine App.vue :

<script>
import Menu from './components/Menu'
import Beer from './components/Beer'

export default {
  name: 'app',
  components: {
    "v-menu": Menu,
    "v-beer": Beer
  },

La balise <menu> étant déjà réservée par HTML 5, nous suffixons les balises avec v-. Ainsi, la balise HTML associée au composant Menu est <v-menu>.

Le composant Menu encapsule le tag <nav> du fichier home.html récupéré lors du PW0 :

<template>
  <nav class="navbar navbar-inverse navbar-fixed-top" role="navigation">
    <div class="container">
      <div class="navbar-header">
        <a class="navbar-brand" href="proxy.php?url=/">Zenika Ecommerce</a>
      </div>
      <div class="navbar-collapse" id="bs-example-navbar-collapse-1">
        <ul class="nav navbar-nav">
          <li>
            <a href="proxy.php?url=#/basket.html">Accéder à votre panier {{bieres.length}} articles</a>
          </li>
        </ul>
      </div>
    </div>
  </nav>
</template>

<script>
export default {
  name: 'menu',
  props: ['bieres']
}
</script>

<style lang="css">
</style>

Pour afficher dynamiquement le nombre de bières ajoutées au panier, on déclare une propriété bieres. Un tableau de bières peut ensuite être passé en paramètre d’entrée du tag <v-menu>  dans le fichier App.vue :

<template>
  <div id="app">
    <v-menu :bieres="panier"></v-menu>

Ici on utilise la syntaxe raccourcie :bieres équivalente à v-bind:bieres. Le panier passé en paramètre référence l’attribut panier du modèle (attribut data) du composant App. Au démarrage de l’application, le panier du client est vide :

export default {
  name: 'app'
  ... 
  data () {
   return {
     panier: []
     }
  }
}

Le composant Beer est responsable d’afficher le détail d’un article/item (dans notre cas une bière) et permet à l’utilisateur de l’ajouter à son panier. Sa déclaration se rapproche de celle du composant Menu.
Son template est créé à partir du div « thumbnail » récupéré du fichier home.html.
Le composant Beer accepte la propriété item (qui n’est autre que la bière à afficher). Un item possède 5 propriétés : label, price, image, description et note. Ces propriétés sont affichés dans le template par la syntaxe {{item.<propriété>}}

<template lang="html">
  <div class="thumbnail">
    <img :src="proxy.php?url=item.image" alt="">
    <div class="caption">
      <h4 class="pull-right">{{item.price}} €</h4>
      <h4><a href="proxy.php?url=#">{{item.label}}</a>
      </h4>
      <p>{{item.description}}</p>
    </div>
    <div class="ratings">
      <button @click="ajouter" type="button" class="pull-right btn btn-primary" aria-label="Ajoutez au Panier">Ajouter</button>
      <p>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star"></span>
        <span class="glyphicon glyphicon-star-empty"></span>
      </p>
    </div>
  </div>
</template>

<script>
export default {
  name: 'produit',
  props: ['item'],
  methods: {
    ajouter () {
      this.$emit('ajout', this.item)
    }
  }
}
</script>

<style lang="css">
</style>

La balise <img>  référence l’URL de l’image représentant la bière passée en item :

<img :src="proxy.php?url=item.image" alt="">

La propriété HTML src n’est pas directement utilisée. Vue.JS l’interdit. La syntaxe :src permet de ne valoriser l’attribut HTML src que lorsque la donnée sera disponible. En effet, la donnée peut provenir d’une API REST et pendant quelques ms ou secondes la donnée peut être undefined. On retrouve la même problématique en Angular.

Outre l’affichage dynamique des propriété d’un item, le composant Beer permet d’ajouter la bière au panier. Lorsque l’utilisateur clique sur le bouton « Ajouter », on fait appel à la méthode ajouter() du composant :

<button @click="ajouter" type="button" >Ajouter</button>

Est utilisé ici une syntaxe raccourcie de v-on:click= »ajouter » .
La méthode ajouter émet un événement au composant parent App :

ajouter () {
  this.$emit('ajout', this.item)
}

‘ajout’ correspond au nom de l’événement et this.item à la donnée associée à l’événement, ici la bière à ajouter au panier. Dans le composant parent App, il est désormais possible de s’abonner à l’événement ‘ajout’. Nous y reviendrons.
A noter que le mot clé this correspondant à l’instance de la Vue.

Dans le composant App, le tag <v-beer>  peut désormais être utilisé pour afficher chacune des bières. Dans un 1er temps, le tableau de bières est hard-codé sous forme d’un tableau d’objets JavaScript déclaré en tant que propriété produits du composant App :

data () {
  return {
    panier: [],
    produits: [
      {
        'label': 'Queue de Charrue',
        'price': '3.70',
        'image': '/static/images/queuedecharrue.jpg',
        'description': 'La Queue de Charrue est une famille de bières brassées pour la Brasserie Vanuxeem. La plus connue et typique est la Queue de Charrue brune. Son nom ...',
        'note': 4
      },
      ...
    ]
  }

Le tableau de produits est construit par copier/coller du fichier beers.json.

Dans le template du composant App, n’ayant pas encore appris comment itérer sur un tableau, le tag <v-beer>  est répété 4 fois :
<div class= »col-sm-4 col-lg-4 col-md-4″>

<div class="col-sm-4 col-lg-4 col-md-4">
    <v-beer v-on:ajout="ajoutPanier" :item="produits[0]"></v-beer>
</div>
<div class="col-sm-4 col-lg-4 col-md-4">
    <v-beer v-on:ajout="ajoutPanier" :item="produits[1]"></v-beer>
</div>
…

Chaque bière est référencée par son index dans le tableau produits. Elles sont passées en paramètres du composant Beer par la propriété item.

La directive v-on positionne le handler ajoutPanier sur l’écoute de l’événement ajout émis par le composant Beer.
L’implémentation de la méthode ajouterPanier ne pose aucune difficulté :

methods: {
 ajoutPanier: function (biere) {
   this.panier.push(biere)
 }
},

Lorsqu’une bière est ajoutée au panier, le modèle panier de la vue est modifié. A l’écran, le nombre d’articles du panier est automatiquement rafraichi.

Bravo, vous venez de terminer l’étape PW2.

PW3 – Utilisation des directives

Beaucoup plus simple et court que le précédent, ce 3ième exercice consiste à mettre en œuvre 3 directives proposées par Vue.js.
Commencez par prendre connaissance des slides 4-1 à 4-3 puis suivez l’énoncé.

Solution

La directive v-for permet d’itérer sur la liste des bières afin d’afficher autant de composants Beer.vue qu’il y a d’éléments dans le tableau :

<div v-for="beer in produits" class="col-sm-4 col-lg-4 col-md-4">
  <v-beer v-on:ajout="ajoutPanier" :item="beer"></v-beer>
</div>

Contrairement à Angular 2, Vue.js offre la possibilité d’utiliser directement les directives sur le tag <v-beer>  (et non pas seulement sur le <div> englobant) :

<v-beer v-for="beer in produits" v-on:ajout="ajoutPanier" :item="beer>

Dans le tableau de Beer, l’ajout d’une propriété stock initialisée à 5 pour tous les éléments se fait sans difficulté :

{
  'label': 'Queue de Charrue',
  'price': '3.70',
  'image': '/static/images/queuedecharrue.jpg',
  'description': '...'
  'note': 4,
  'stock': 5
},

Lorsqu’une bière est ajoutée au panier, on décrémente son stock :

ajouter () {
  this.item.stock--;
  this.$emit('ajout', this.item)
}

En utilisant la directive v-if, nous pouvons désormais n’afficher à l’utilisateur que les bières en stock :

<v-beer v-for="beer in produits" v-if="beer.stock > 0" v-on:ajout="ajoutPanier" v-bind:item="beer"></v-beer>

Pour changer la couleur de fond d’un produit lorsque son stock atteint 1, on commence par déclarer la classe CSS last dans la section <style> de App.vue :

<style lang="css">
.last {
    background-color: rgba(255, 0, 0, 0.4)
}
</style>

Sur le div possédant la classe thumbnail, en utilisant la directive v-bind:class , nous pouvons ensuite ajouter la classe last lorsque le stock de bière est de 1 :

<div class="thumbnail" :class="{ last: item.stock == 1 }">

Une seconde solution consiste à utiliser les computed values. Une computed value s’utilise comme une propriété mais se définit comme une méthode. Son résultat est mis en cache par Vue.js. Nous déclarons la computed value thumbnailClass dans App.vue :

export default {
  name: 'produit',
  props: ['item'],
  computed: {
    thumbnailClass() {
      return this.item.stock === 1 ? 'thumbnail last' : 'thumbnail';
    }
  },
...

Son utilisation dans le template HTML se fait par binding :

<div :class="thumbnailClass">

Les computed values peuvent également être utilisées pour afficher le coût total du panier dans Menu.vue. Le total est calculé à partir du prix de chaque bière :

<script>
export default {
  name: 'menu',
  props: ['bieres'],
  computed: {
    total: function () {
      let total = 0
      this.bieres.map((biere) => {
        total += parseFloat(biere.price)
      })
      return Math.round(total * 100) / 100
    }
  }
}
</script>

La computed value total est ensuite utilisée dans le template comme s’il s’agissait d’une propriété :

<a href="proxy.php?url=#/basket.html">Accéder à votre panier ({{bieres.length}} articles - {{total}} €)</a>

Pour afficher une mention différente lorsque le panier est vide, on utilise de nouveau la directive v-if :

<a v-if="bieres.length > 0" href="proxy.php?url=#/basket.html">Accéder à votre panier ({{bieres.length}} articles - {{total}} €)</a>
<a v-if="bieres.length === 0" href="proxy.php?url=#/basket.html">Accéder à votre panier (vide)</a>

 

PW4 – Les filtres

Les slides 5-2 et 5-3 expliquent comment créer un filtre global à toute l’application et un filtre local au composant.
Lors du Lab, nous n’avons pas eu le temps de coder cet exercice. Je vous décrirai donc uniquement comment créer et utiliser le premier filtre uppercase demandé.

Commencez par créer le fichier src/filters/uppercase.js :

import Vue from 'vue'

Vue.filter('uppercase', function (value) {
  if (!value) return ''
  value = value.toString()
  return value.toUpperCase()
})

Importez ce fichier dans main.js :

import './filters/uppercase'

Puis, avec la même syntaxe qu’Angular, utilisez le filtre dans Beer.vue :

<h4><a href="proxy.php?url=#">{{ item.label | uppercase }}</a>

 

PW5 – Les Ressources

Cet exercice propose d’utiliser une API REST pour récupérer la liste des bières à afficher. Le module vue-resource présenté dans les slides 6-1 à 6-9 va vous y aider.
Comme vous le verrai, l’API de $http ressemble beaucoup à celle de AngularJS. Les méthodes get, head, delete, post retournent une promesse. Elles acceptent 2 callback : l’une en cas de succès et l’autre en cas d’erreur.

Les intercepteurs permettent de transformer une requête http, par exemple pour ajouter des headers HTTP.

Pour résoudre cet exercice, il vous sera utile de savoir qu’un composant possède un cycle de vie et qu’on peut brancher du code à chaque étape. Comme son nom d’L’étape created() permet d’exécuter du code initialisant les données du composant.

Solution

Commencez par démarrer le serveur REST avec Node.JS puis testez son API depuis le navigateur.
Extrait de la réponse observée lors d’un GET sur http://localhost:1337/api/v1/beers  :

[
  {
    "label": "Queue de Charrue",
    "description": "La Queue de Charrue est une famille de bières brassées pour la Brasserie Vanuxeem. La plus connue et typique est la Queue de Charrue brune. Son nom ...",
    "image": "/static/images/queuedecharrue.jpg",
    "price": 3.7,
    "stock": 2
  },
…

Installez le module vue-resource via la commande npm :

npm install vue-resource --save

Dans le fichier main.js, importez le module vue-resource :

import VueResource from 'vue-resource';

Vue.use(VueResource);

Dans App.vue, implémentez la méthode getProduits() faisant appel à l’API REST. Cette méthode est appelée pendant le cycle de vie du composant, une fois celui-ci créé :

created() {
   this.getProduits();
},
methods: {
   getProduits() {
   this.$http.get('http://localhost:1337/api/v1/beers')
           .then(response => { this.produits = response.body; });
 },

On initialise à vide le tableau de bières précédemment hard-codé :

produits: []

Conclusion

Les plus motivés pourront poursuivre les exercices PW6, PW7 et PW8 du Lab. Vous y apprendrez comment configurer un routeur, valider des formulaires et gérer de manière centralisée l’état d’une application. Les différentes branches Git permettent d’avoir accès aux corrections.

Vue.js se veut simple. Ce Lab nous l’aura confirmé. Reprenant de nombreux concepts de ses pairs, il ne dépaysera pas les développeurs front.
Vue.js se veut également léger. C’est le cas si l’on se restreint à son noyau. Néanmoins, dès que l’on développe une application web un peu conséquente, on doit y ajouter des modules tiers : vue-resource, vee-validate, vuex … L’avantage de Vue.js est d’être flexible et de nous laisser choisir ces différentes briques. Il se veut beaucoup moins structurant qu’un Angular.

Pour terminer, un grand merci à Aurélien et Emmanuel pour cette découverte. Et à l’an prochain pour ne nouveau framework JS du moment !!

Resources :

]]>
https://javaetmoi.com/2017/04/codez-lab-vue-js-devoxx-france-2017/feed/ 0
Devoxx France 2016 : une conf appréciée, un slide https://javaetmoi.com/2016/05/devoxx-france-2016-un-slide-par-conf/ https://javaetmoi.com/2016/05/devoxx-france-2016-un-slide-par-conf/#respond Sat, 14 May 2016 09:10:07 +0000 http://javaetmoi.com/?p=1608 Voici la présentation que j’ai animée auprès de mes collègues afin de leur faire un retour suite à ma participation à Devoxx France 2016. Le leitmotiv était « 1 conférence appréciée => 1 slide ».
Au menu : Angular 2, ECMASript 2015, Kakfa, Spring Cloud, architecture StackOverflow, Jenkins pipeline, React, revues de code et documentation.


]]>
https://javaetmoi.com/2016/05/devoxx-france-2016-un-slide-par-conf/feed/ 0
16 prises de notes à Devoxx France 2016 https://javaetmoi.com/2016/05/16-prises-de-notes-a-devoxx-france-2016/ https://javaetmoi.com/2016/05/16-prises-de-notes-a-devoxx-france-2016/#respond Thu, 12 May 2016 17:14:54 +0000 http://javaetmoi.com/?p=1580 Continuer la lecture de 16 prises de notes à Devoxx France 2016 ]]> Pour vous aider à choisir quelle conférence visionner sur la chaîne Devoxx FR 2016 de Youtube ou pour vous remémorer certaines chose, je mets librement à votre disposition les différentes notes que j’ai pu prendre sur mon laptop.

Les sujets sont variés : des Microservices avec Spring Boot et Spring Cloud, du Big Data avec Kafka et Elasticsearch, du Front End avec ECMAScript 2015 et React, du Java 8 et 9 ou bien encore de la méthodologie avec les revues de code et de la living documentation.

Certaines notes pourront être lues de manière autonome ; je pense par exemple au quickie Comment rater ses revues de code ? et à la conférence Stack Overflow behind the scenes. Pour être exploitables en l’état, d’autres notes demanderont à ce que vous ayez assisté à la conférence ou que vous ayez pu récupérer les supports de présentation.

2016_04_21_Devoxx_France_2016_keynote

Sans plus attendre, voici donc mes 16 notes triées par ordre alphabétique :

  1. Bootiful Microservice
  2. Comment faire tourner une JVM de 16 To ?
  3. Comment rater ses revues de code ?
  4. DDD : et si on reprenait tout depuis le bon bout ?
  5. E6+ maintenant !
  6. Elasticsearch et Hibernate sont sur un bateau
  7. Hand’s On Lab Kafka
  8. Hibernate tu connais … mais en fait tu connais pas
  9. High Performance Hibernate
  10. De Jenkins Maven/ Freestyle à Pipeline
  11. Le design d’API REST, un débat sans fin ?
  12. Let’s React
  13. Live documentation
  14. Retour d’expérience sur Java 8
  15. Stack Overflow behind the scenes. How it’s made
  16. String Concaténation de 1 à 9
]]>
https://javaetmoi.com/2016/05/16-prises-de-notes-a-devoxx-france-2016/feed/ 0
Introduction à Angular JS https://javaetmoi.com/2015/05/introduction-a-angularjs/ https://javaetmoi.com/2015/05/introduction-a-angularjs/#respond Wed, 20 May 2015 17:36:21 +0000 http://javaetmoi.com/?p=1316 Continuer la lecture de Introduction à Angular JS ]]> Si vous pensez encore que le data-binding, l’inversion de dépendances, le pattern MVC ou bien encore la gestion de la navigation sont réservés au code Java des applications web modernes, courrez visionner cette présentation.
AngularJS, le dernier framework JavaScript de chez Google, devrait vous surprendre.

Basée sur l’université  AngularJS, ou le futur du développement Web présentée lors de Devoxx France 2013, cette présentation a pour objectif de vous initier à AngularJS.
Les concepts fondamentaux seront mis en en action au travers de l’application Game Store.
Côté industrialisation, nous verrons que tests unitaires, tests fonctionnels et infrastructure de build ne sont pas non plus délaissés.

A l’heure où la couche présentation des applications web est de plus en plus déportée côté client, ce workshop a pour objectif de démystifier ce type de framework permettant de structurer une application JavaScript.

]]>
https://javaetmoi.com/2015/05/introduction-a-angularjs/feed/ 0
Tester le code JavaScript de vos webapp Java https://javaetmoi.com/2014/03/tester-code-javascript-webapp-java/ https://javaetmoi.com/2014/03/tester-code-javascript-webapp-java/#comments Tue, 04 Mar 2014 09:27:15 +0000 http://javaetmoi.com/?p=995 Continuer la lecture de Tester le code JavaScript de vos webapp Java ]]> tester-code-javascript-webapp-logoVous développez une application web en Java. Le couche présentation est assurée typiquement par un framework MVC situé côté serveur : Spring MVC, Struts 2, Tapestry ou bien encore JSF.  Votre projet est parfaitement industrialisé : infrastructure de build sous maven, intégration continue, tests unitaires, tests Selenium, analyse qualimétrique via Sonar.

A priori, vous n’avez rien à envier à la richesse grandissante de l’écosystème JavaScript, de l’outillage et des frameworks MV* côté clients. Et pourtant, quelque chose vous manque cruellement. En effet, depuis que RIA et Ajax se sont imposés, votre application Java contient davantage de code JavaScript qu’il y’a 10 ans. S’appuyant sur des librairies telles que jQuery ou Underscore, ce code JavaScript est typiquement embarqué dans votre WAR. Pour le valider, les développeurs doivent démarrer leur conteneur web et accéder à l’écran sur lequel le code est utilisé. Firebug ou Chrome sont alors vos meilleurs amis pour la mise au point du script.

Ce code JavaScript n’est généralement pas documenté. Le tester manuellement demande du temps.  Les modifications sont sources d’erreur. Tout changement est donc périlleux. Si, à l’instar de vos tests JUnit pour vous classes Java, vous disposiez de tests JavaScript, vous en seriez comblés. Or, c’est précisément ce qu’il vous manque. Et c’est là où Jasmine et son plugin maven viennent à votre rescousse.

Présentation de Jasmine

Les développeurs AngularJS le connaissent déjà. Mis au point par Pivotal (ex SpringSource), Jasmine est un framework de tests unitaires pour JavaScript et CoffeeScript. Contrairement à QUnit qui est a été initialement créé pour tester le projet jQuery, Jasmine est indépendant de tout framework JavaScript. Son principal avantage réside dans le fait qu’il ne nécessite pas de navigateur pour exécuter les tests : un simple moteur JavaScript suffit.

Complet, Jasmine offre tout l’outillage nécessaire à l’écriture de tests :

Fonctionnalité Description Frameworks Java équivalent
Structuration des tests Suite de tests (describe), fonctions de tests (it), setUp et tearDown, ignore JUnit, TestNG
Matchers Fonctions utilisées pour les assertions : expect, toEqual, toBe, not, toBeDefined … Feist Assert, JUnit, Hamcrest
Bouchons Création d’espions et de simulacres: createSpy, andReturn, andCallFake, toHaveBeenCalled … Mockito

Vous expliquez ici comment écrire des tests avec Jasmine dépasse le cadre de cet article. Je vous renvoie à la documentation officielle et au tutorial Testing JavaScript using the Jasmine framework.

Dans la suite de cet article, les fichiers Player.js, Song.js, PlayerSpec.js et SpecHelper.js issus de la version standalone de Jasmine sont utilisés comme jeu d’exemple.
Pour les adeptes de jQuery, le projet jasmine-jquery étend Jasmine de 2 manières :

  1. Ajout de matchers liés au DOM : toBeSelected, toContainText, toHaveClass, toContainHtml, toBeVisible …
  2. Initialisation du DOM à partir d’un fichier HTML lors de l’étape de fixture.

Le fichier PasswordSpec.js en montre un exemple d’utilisation.

Intégration de Jasmine à maven

Le plugin pour maven jasmine-maven-plugin permet d’exécuter vos tests Jasmine (aussi appelés specs) lors de la phase de test de votre build maven.

tester-code-javascript-webapp-arboLe pom.xml du projet jasmine-test-webapp donne un exemple de configuration du plugin. Pour mieux comprendre sa configuration, vous présenter l’organisation du projet est nécessaire.

L’arborescence du projet suit les conventions maven d’un war. Pages dynamiques et ressources statiques se trouvent dans le répertoire src/main/webapp.

Par choix, Le code JavaScript spécifique à l’application est localisé dans le sous-répertoire static/js/app du répertoire src/main/webapp. C’est ce code qui doit être testé.
Les librairies tierces comme ici jQuery sont placées dans un sous-répertoire static/js/lib.

Les fichiers de tests JavaScript sont quant à eux placés dans le répertoire src/test/javascript. Pour utiliser l’extension jasmine-jquery dans les specs, la seule présence du fichier jasmine-jquery.js suffit.

La configuration associée du jasmine-maven-plugin est la suivante :

Configuration du jasmine-maven-plugin

Remarque : afin que l’objet $ soit défini lors du chargement du script Password.js, la librairie jQuery est chargée en premier.

Voici le résultat de la sortie console de la commande mvn test  :

[INFO] ------------------------------------------------------------------------
[INFO] Building JavaEtMoi Samples :: jasmine-test-webapp - war 1.0-SNAPSHOT
[INFO] ------------------------------------------------------------------------
...
[INFO] 
[INFO] --- maven-surefire-plugin:2.10:test (default-test) @ jasmine-test-webapp ---
[INFO] No tests to run.
...
[INFO] --- jasmine-maven-plugin:1.3.1.4:test (default) @ jasmine-test-webapp ---
[INFO] Executing Jasmine Specs
[INFO] 
-------------------------------------------------------
 J A S M I N E   S P E C S
-------------------------------------------------------
[INFO] 
password validation label
  should become red when password is too short
  should be green when password length is more then 6 symbols
Player
  should be able to play a Song
  when song has been paused
    should indicate that the song is currently paused
    should be possible to resume
  tells the current song if the user has made it a favorite
  #resume
    should throw an exception if song is already playing
Results: 7 specs, 0 failures
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 14.035s
[INFO] Finished at: Sat Mar 01 19:26:03 CET 2014
[INFO] Final Memory: 10M/247M
[INFO] ------------------------------------------------------------------------

Mise à part l’installation de Java et de Maven, aucun prérequis n’est nécessaire. Magique, vous ne trouvez pas ? Mais alors, comment est interprété le code JavaScript ? Et qui met à disposition le DOM manipulé dans le JavaScript ?
En plus de Jasmine, le jasmine-maven-plugin embarque HmtlUnitDriver, une implémentation de Selenium WebDriver. En interne, HmtlUnitDriver s’appuie sur HtmlUnit pour le DOM et sur Rhino pour le JavaScript. Tous les deux sont écrits en Java et sont Open Source. Initié par la fondation Mozilla, le moteur JavaScript Rhino a été intégré à Java 6.
Respectant le standard HTML, HmtlUnitDriver permet d’émuler des spécificités de certains navigateurs.
Jasmine-maven-plugin permet de tirer parti cette fonctionnalité. A sa configuration, il est possible d’ajouter la balise suivante : <browserVersion>FIREFOX_17</browserVersion>

Behavior-Driven Development

Le plugin jasmine-maven-plugin permet de développer en TDD sans avoir à lancer un clean test à chaque changement de code.
La commande mvn jasmine:bdd  lance un serveur web qui scrute tout changement dans le répertoire du code JavaScript et des tests JavaScript. Rafraichir la fenêtre de son navigateur permet de réexécuter les tests.

tester-code-javascript-webapp-bdd

Couverture de tests

A présent que jasmine-maven-plugin est en place sur votre projet, vous pouvez en profiter pour générer à moindre coût la couverture du code JavaScript testé. En effet,  ce plugin s’interface avec le saga-maven-plugin.
Par la ligne de configuration suivante, on indique au jasmine-maven-plugin de ne pas arrêter le serveur web une fois les tests unitaires exécutés :

<keepServerAlive>true</keepServerAlive>

La configuration du saga-maven-plugin est triviale et très bien documentée :

Configuration du saga-maven-plugin

Des rapports Cobertura et HTML sont générés dans le sous-répertoire target/coverage pendant la phase verify de maven :
tester-code-javascript-webapp-saga

Utilisation de PhantomJS

Le principal inconvénient de la solution présentée jusqu’ici est que ni HtmlUnit ni Rhino ne sont  utilisés par un quelconque navigateur du marché. Comment être certain que votre code s’exécute sur un Chrome ou un Safari ? C’est là que PhantomJS rentre en jeu. En effet, PhantomJS est un navigateur headless (sans interface graphique) basé sur WebKit (le moteur HTML de Safari, d’Opera et du fork de Chrome). Le plugin Jasmine pour maven permet d’utiliser PhantomJS à la place de HtmlUnit.

Pour utiliser PhantomJS, il est tout d’abord nécessaire de l’installer. Une archive existe pour chaque OS supporté : Windows, MacOSX, Linux 32 bits et 64 bits. Afin que maven puisse installer l’archive PhantomJS de l’OS sur lequel le build est exécuté, il est possible d’uploader ces archives dans votre repo d’entreprise (ex : Nexus, Artifactory). Voici un exemple de commande maven :

mvn deploy:deploy-file -DgroupId=org.phantomjs -DartifactId=phantomjs -Dversion=1.9.7 -Dpackaging=zip -Dclassifier=windows -Dfile=phantomjs-1.9.7-windows.zip -DrepositoryId=javaetmoi-cloudbees-release -Durl=https://repository-javaetmoi.forge.cloudbees.com/release/

Une fois déployées, les archives sont disponibles dans le répertoire org/phantomjs/phantomjs/1.9.7/

Le goal unpack du plugin maven-dependency-plugin permet d’installer PhantomJS dans le répertoire target/ pendant la phase initialize de la commande mvn test  :

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-dependency-plugin</artifactId>
  <executions>
    <execution>
      <phase>initialize</phase>
      <goals>
        <goal>unpack</goal>
      </goals>
      <configuration>
        <artifactItems>
          <artifactItem>
            <groupId>org.phantomjs</groupId>
            <artifactId>phantomjs</artifactId>
            <version>1.9.7</version>
            <type>zip</type>
            <classifier>windows</classifier>
            <outputDirectory>${project.build.directory}</outputDirectory>
          </artifactItem>
        </artifactItems>
      </configuration>
    </execution>
  </executions>
</plugin>

Une autre solution permettant d’installer automatiquement PhantomjS consiste à utiliser le plugin PhantomJS pour maven.

Enfin, pour substituer PhantomJS à HtmlUnit, le jasmine-maven-plugin doit être configuré de la manière suivante :

<webDriverClassName>org.openqa.selenium.phantomjs.PhantomJSDriver</webDriverClassName>
<webDriverCapabilities>
  <capability>
    <name>phantomjs.binary.path</name>
    <value>${project.build.directory}/phantomjs-1.9.7-windows/phantomjs.exe</value>
  </capability>
</webDriverCapabilities>

Lorsque des tests échouent, les erreurs remontées par PhantomJS sont, de manière générale, plus compréhensibles que leurs homologues HtmlUnit.
Voici un exemple de message d’erreur lorsqu’on oubli de rajouter la librairie underscore. js et que la variable globale _ n’est pas définie :

Avec HtmlUnit :

[ERROR] java.lang.RuntimeException: org.openqa.selenium.WebDriverException: com.gargoylesoftware.htmlunit.ScriptException: TypeError: Impossible dappeler la méthode "{1}" de {0} (script in http://localhost:3213/ from (6, 34) to (15, 12)

Avec PhantomJS :

\[WARNING\] JavaScript Console Errors:
&nbsp; * TypeError: 'undefined' is not an object (evaluating '_.each')

Le pom.xml utilise les profiles maven pour utiliser à la demande PhantomJS. Il installe la version de PhantomJS correspondant à l’OS sur lequel la commande maven est exécutée. Le profile phantomJS permet d’exécuter les tests avec PhantomJS :
mvn test -PphantomJS

Conclusion

Au travers de ce billet, nous avons vu comment intégrer l’exécution de tests unitaires JavaScript dans une infrastructure de build basée sur maven. L’intégration de Jasmine avec maven est telle qu’il n’est nullement nécessaire de mettre en place des outils du monde JavaScript basés sur Node.JS tel Grunt ou Gulp.
Avec le plugin Jasmine pour maven, vos tests unitaires peuvent aussi  bien tester des fonctions métiers que des fonctions manipulant le DOM du navigateur. En outre, l’intégration de PhantomJS permet de se garantir que le code JavaScript testé fonctionnera sur les navigateurs basés sur WebKit.

Vos tests unitaires JavaScript peuvent dès à présent intégrer votre plateforme d’intégration continue. Preuve en est, les tests unitaires du projet jasmine-test-webapp ont été exécutés par Travis CI  et par le Jenkins de CloudBees.

]]>
https://javaetmoi.com/2014/03/tester-code-javascript-webapp-java/feed/ 1
Développer et industrialiser une web app avec AngularJS https://javaetmoi.com/2014/02/developper-industrialiser-web-app-recherche-angularjs/ Sun, 09 Feb 2014 19:13:56 +0000 http://javaetmoi.com/?p=898 Continuer la lecture de Développer et industrialiser une web app avec AngularJS ]]> Au travers du billet Elastifiez la base MusicBrainz sur OpenShift, je vous ai expliqué comment indexer dans Elasticsearch et avec Spring Batch l’encyclopédie musicale MusicBrainz. L’index avait ensuite été déployé sur le Cloud OpenShift de RedHat.
Une application HTML 5 était mise à disposition pour consulter les albums de musique ainsi indexés. Pour m’y aider, Lucian Precup m’avait autorisé à adapter l’application qu’il avait mise au point pour l’atelier Construisons un moteur de recherche  de la conférence Scrum Day 2013.
Afin d’approfondir mes connaissances de l’écosystème JavaScript, je me suis amusé à recoder cette application front-end en partant de zéro. Ce fut l’occasion d’adopter les meilleures pratiques en vigueur : framework JavaScript MV*, outils de builds, tests,  qualité du code, packaging …
Au travers de ce article, je vous présenterai comment :

  1. Mettre en place un projet Anguler à l’aise d’Angular Seed, Node.js et Bower
  2. Développer en full AngularJS et Angular UI Bootstrap
  3. Utiliser le framework elasticsearch-js
  4. Internationaliser une application Angular
  5. Tester unitairement et fonctionnellement une application JS avec Jasmine et Karma
  6. Analyser du code source JavaScript avec jshint
  7. Packager avec Grunt le livrable à déployer
  8. Utiliser l’usine de développement JavaScript disponible sur le Cloud : Travis CI, Coversall.io et David

Le code source de l’application est bien entendu disponible sur GitHub et testable en ligne.

Angular MusicBrainz web app screenshot

Démarrer un projet avec Angular Seed

Hébergé sur GitHub et maintenu par les auteurs d’Angular, le projet angular-seed permet de démarrer  rapidement une application Angular. Outre le squelette applicatif, ce projet propose :

  • des exemples de tests unitaires et de tests dits end-to-end,
  • des scripts .sh ou .bat permettant d’exécuter ces différents types de tests
  • un script JS permettant de démarrer un serveur web sous NodeJS

Le README.MD explique de manière approfondie l’organisation du projet et la nature de chaque fichier.
Une fois ce repository cloné sous GitHub ou bien dézippé en local, il est possible de le personnaliser à sa guise.

Une alternative à angular-seed serait d’utiliser Yo pour générer le squelette de l’application, sur un principe similaire aux archetypes maven.

Exécuter l’application

L’application blanche fournie dans Angular Seed étant une application full HTML, il n’est pas nécessaire de la déployer dans un serveur d’application JEE ou un conteneur web. Un  simple serveur web comme Apache ou Nginx est nécessaire.
Les utilisateurs de Firefox pourront même se passer de serveur web et ouvrir directement le fichier app/index.html à partir de leur disque.
Chrome n’ayant pas cette faculté (les requêtes Ajax chargées de récupérer un fichier sur disque sont bloquées), vous pouvez utiliser le serveur web installé sur votre poste de développement.

En guise de serveur web, vous pourrez utiliser le script scripts\web-server.js pour en démarrer un en full JavaScript. Le seul pré-requis est l’installation de NodeJS qui intègre le moteur JavaScript V8 de Google. A noter que NodeJS et son gestionnaire de paquets npm seront nécessaires dans la suite de cet article pour installer les outils, construire l’application, monter de version les dépendances ou bien encore exécuter les tests.

Une fois NodeJS installé et ajouté au PATH du système, exécuter la commande suivante pour démarrer le serveur:

D:\Dev\angular-musicbrainz>node scripts\web-server.js
Http Server running at http://localhost:8000/

Saisir l’URL http://localhost:8000/app/index.html dans le navigateur de votre choix. Les requêtes HTTP apparaissent sur la console :

GET /app/index.html Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
GET /app/lib/bootstrap/dist/css/bootstrap.css Mozilla/5.0d (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.36
GET /app/lib/bootstrap/dist/css/bootstrap-theme.css Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/53
GET /app/lib/angular-resource/angular-resource.js Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/31.0.1650.63 Safari/537.
…

 Structuration du projet

La structure des répertoires du projet reprend celle d’Angular Seed.  Le tableau ci-dessous liste les librairies tierces utilisées par l’application.

Répertoires / fichiersDescription
app/Code source et ressources de l’application
        css/Feuilles de styles CSS
        i18n/Fichiers JSON de traduction de l’application
        img/Images et icône
        index.htmlPage principale de la Single Page Application
        js/Fichiers JavaScript spécifiques à l’application
                app.jsDéclaration des modules et démarrage de l’application
                controllers.jsContrôleurs Angular spécifiques à l’application
                directives.jsDirectives Angular spécifiques à l’application
                filters.jsFiltres / formateurs Angular spécifiques à l’application
                routes.jsConfiguration des routes
                services.jsServices spécifiques à l’application
        lib/Librairies tierces déclarées dans bower.json
                angular/Module principal d’Angular
                angular-i18n/                Fichiers de traductions fournis par Angular
                angular-mocks/                Mocks permettant de bouchonner des services Angular
                angular-resource/                Accès REST aux ressources serveur
                angular-route/Routage des vues en fonction de l’URL
                angular-sanitize/Filtres standards
                angular-scenario/DSL des scénarios de tests end-to-end
                angular-ui-boostrap-bower/Widgets Angular basés sur Bootstrap
                bootstrap/Mise en page et charte graphique
                elasticsearch-js/Client JavaScript pour Elasticsearch
                jquery/Manipulation de DOM
        partials/        Template HTML des vues de l’application
                directives/Template HTML des directives de l’application
        conf/Configuration Karma des tests unitaires et e2e
        dist/Répertoire de destination du livrable de production
        node_modules/Modules NodeJS utilisées par Grunt
        scripts/Scripts shell, JS et batch permettant de lancer les tests et de démarrer un serveur web
        test/Code source des tests
                e2e/Tests end-to-end
                unit/Tests unitaires

Automatisation avec Grunt

Grunt peut être comparé au Gradle du monde JavaScript. Il permet d’exécuter des tâches sous NodeJS et est particulièrement utile pour automatiser certaines tâches de développement. Son installation nécessite une seule commande :
npm install -g grunt-cli

Le gestionnaire npm permet ensuite de télécharger et d’installer les modules NodeJS nécessaires au fonctionnement du script Grunt. Commande à exécuter à la racine du répertoire, au même niveau que le fichier package.json :
npm install

Le sous-répertoire node_modules est alimenté par les modules déclarés dans le fichier package.json.

Angular Seed ne configurant pas Grunt, je me suis inspiré de différents exemples pour mettre au point le script Gruntfile.js.

Voici quelques commandes utiles :

  • grunt test : lance successivement les tests unitaires et les tests end-to-end
  • grunt server : démarre un serveur web, ouvre la page dans l’application et, à l’instar de JRebel, recharge à chaud le code modifié depuis votre IDE.
  • grunt jshint : vérifie que la qualité du code de production JavaScript
  • grunt build : construit le livrable à installer sur le serveur web. Les fichiers générés sont mis à disposition dans le sous-répertoire dist.
  • grunt karma:coverage : génère le taux de couverture des tests unitaires. Au format HTML, le rapport est accessible depuis le sous-répertoire coverage\PhantomJS 1.9.2 (Windows 7)\lcov-report

Pendant le développement de l’application web, à des fins de débogage, le code JavaScript est non minifié et séparé dans plusieurs fichiers, de même pour les feuilles de style CSS.
Sur un principe similaire à ce que propose Jawr dans le monde Java, l’étape de build va permettre d’obtenir un livrable le plus léger possible. Voici les opérations effectuées :

  1. Le code JavaScript et les feuilles de styles CSS sont concaténées puis minifiés. Exemple de directive à placer dans le code HTML :
    Directive build:js sur la page index.html

  2. Les images bitmaps et vectorielles sont réduites.
  3. Les pages HTML référençant ces ressources statiques sont mises à jour en conséquences.

Dépendances Bower

Comme vu au paragraphe précédent, les modules nécessaires au fonctionnement de l’infrastructure de build basé sur Grunt sont récupérés à l’aide de npm et du fichier package.json.

Les librairies nécessaires au développement de l’application (Angular, Bootstrap) et de ses tests sont gérées par un second système de dépendances, à savoir Bower. Cet outil permet de rechercher des librairies via la commande  bower search <nom-librairie>. L’instruction bower install <nom- librairie> –save(-dev) permet de la télécharger et de la référencer dans le fichier bower.json. Les librairies sont installées dans le répertoire app/lib configuré dans le fichier .bowerrc.

S’appuyant sur le fichier bower.json, la commande bower install permet de récupérer toutes les dépendances utilisées par l’application. Elle permet également de mettre à jour les dépendances lors d’une montée de version.

En théorie, il n’est pas nécessaire d’archiver ces dépendances dans le gestionnaire de code source (ici GitHub). En pratique, c’est utile de pouvoir exécuter l’application sans avoir à installer ni NodeJS ni Bower.

Architecture applicative

En avril 2013, je découvrais Angular à Devoxx France. Je vous en présentais ici même les mécanismes et les fonctionnalités clés. Après m’être auto-formé et avoir animé un workshop sur ce framework, j’étais impatient de le mettre en œuvre sur un projet personnel (en attendant un futur projet pro). Et autant vous l’annoncer dès à présent, je n’ai pas été déçu : j’ai pris un réel plaisir à développer le front end de mon application de recherche angular-musicbrainz.

D’un point de vue de l’architecture applicative, cette single page application est composée de 3 fichiers HTML principaux :

  1. index.html : bootstrape l’application en initiant le gabarit HTML (essentiellement le menu) et en téléchargeant les fichiers JavaScript, HTML et CSS nécessaires à son fonctionnement
  2. partials\search.html : vue de recherche et de restitution des résultats. C’est l’implémentation de cette vue que nous allons présenter dans la suite de se billet.
  3. partials\info.html : vue référençant quelques URLs utiles.

Le code JavaScript de l’application est découpé techniquement. A une couche technique (contrôleurs, services, filtres et directives) correspond un fichier JavaScript et un module Angular.
Simple,  cette organisation est pratique pour de petites applications. Sur des applications plus conséquences, un découpage fonctionnel est à privilégier.

Services

L’application repose sur 4 services implémentés dans le fichier services.js :

Nom du serviceFonctionnalités
esCrée et configure un client Elasticsearch. S’appuie sur la fabrique mise à disposition par la librairie elasticsearch-js.
searchServiceCode métier regroupant les requêtes JSON de recherche Elasticsearch. 2 méthodes sont disponibles : l’une pour la recherche full text et l’autre pour l’auto-complétion.
userLanguagePermet d’accéder à la locale de l’utilisateur.
translationRécupère le fichier de traduction de l’application.

Filtres

Les templates HTML reposent sur 5 filtres personnalisés

Nom du filtreUtilisationRendu
interpolate{ ‘v%VERSION%’ | interpolate }V1.0
joinBy{{hit._source.tags | joinBy:’ – ‘}}pop – rock – blues
reverseng-repeat= »rating in facets.rating.entries | reverse » angular-musicbrainz-rating
artistTypeLabel{{type.term | artistTypeLabel}}Artiste (fr) ou Artist (en)
yearFormat{{range | yearFormat}}Avant 1970 (fr)

Pour les détails d’implémentation, se référer au code source filters.js et aux tests unitaires qui les documentent filterSpecs.js.

Directives

2 widgets graphiques ont été réalisés à l’aide de directives :

DirectiveUtilisationRendu
Cover<cover album-id= »hit._source.id »> </cover> angular-musicbrainz-cover
Rank<rank score= »hit._source.rating.score »> </rank> angular-musicbrainz-rank

A noter que la directive rating d’Angular Bootsrap offre une alternative à la directive rank. Cette dernière reprend la CSS de MusicBrainz permettant d’ajuster le dégradé des étoiles au pixel près.

Afin de rendre le code des directives plus lisible et maintenable, les templates HTML de ces 2 directives ont été externalisés dans des fichiers html dédiés.
Exemple rating.xml :

<span class="inline-rating">
  <span class="star-rating small-star">
     <span style="width:{{score+ceil}}%;" class="current-rating">{{score}}</span>
  </span>
</span>

La configuration des tests unitaires Karma a dû être ajustée en conséquences :

// generate js files from html templates to expose them during testing.
   preprocessors : {
        'app/partials/directives/**/*.html': ['ng-html2js']
    },

Contrôleurs

A  chaque vue de l’application, correspond un contrôleur. Le fichier controller.js en défini donc deux : SearchCtrl et InfoCtrl.
Trivial, le contrôleur InfoCtrl met à disposition dans le scope de le vue info les 2 URLs affichées et formatées  à l’aide de la directive Angular ngLinky 

Contrôleur :

$scope.demoUrl = 'http://angular-musicbrainz.javaetmoi.com/';

Template :

<li>Online Demo: <span ng-bind-html="demoUrl | linky"/></li>

Le contrôleur SearchCtrl embarque toute la logique applicative de l’application. Il offre à la fois des fonctions réagissant aux actions utilisateurs et les données utilisées par Angular lors du rendu de la vue search. En voici les principales :

PropriétéTypeDescription
fullTextSearchfonctionExécute une recherche full text lors du clic sur le bouton « Recherche MusicBrainz».
autocompletefonctionExécute une requête d’auto-complétion à chaque frappe de l’utilisateur dans le zone de recherche.
selectPageFonctionPermet à l’utilisateur de sélectionner une plage de résultats. Exécute une recherche Elasticsearch sur la plage indiquée.
searchRespDonnéeRésultats Elasticsearch d’une recherche fulltext.
pageSizeDonnéeNombre de résultats à afficher à l’écran.
currentPageDonnéePlage de résultats actuellement affichée.
pageSizesDonnéeTailles de plages que l’utilisateur peut choisir.

Routes

Par rapport au template angular-seed, la configuration des routes a été externalisée dans un fichier dédié routes.js.

Application

Le 6ième et dernier module Angular correspond  au module applicatif musicAlbumApp déclaré dans le fichier app.js. Outre la déclaration des modules Angular nécessaires au fonctionnement de l’application, ce module est chargé de déterminer la langue dans laquelle l’interface doit s’afficher puis charger les données adéquates. Nous y reviendrons dans la suite de cet article.

Utilisation d’elasticsearch-js

Pour interroger le cluster Elasticsearch depuis le navigateur, j’ai étudié 3 possibilités : le service natif $http d’Angular, la librairie elastic.js et la librairie elasticsearch-js. Sortie en décembre 2013 et mise en avant par Elasticsearch.org, j’ai choisi d’utiliser cette dernière. Via la création de tickets GitHub, j’ai eu la chance de pouvoir  contribuer à l’amélioration de cette jeune librairie déjà très mature.

Le module esFactory permet de déclarer en quelques lignes un client JavaScript Elasticsearch. Voici les paramètres renseignés :

angular.module('musicAlbumApp.services', ['ngResource'])
    .value('version', '1.0')
    // elasticsearch.angular.js creates an elasticsearch
    // module, which provides an esFactory
    .service('es', ['esFactory', function (esFactory) {
        return esFactory({
            hosts: [
                // you may use localhost:9200 with a local Elasticsearch cluster
                'es.javaetmoi.com:80'
            ],
            log: 'trace',
            sniffOnStart: false
        });
    }])

J’ai volontairement désactivé la fonctionnalité de sniffOnStart. En effet, j’ai configuré le reverse proxy Nginx pour ne laisser passer que les requêtes de _search. Les requêtes HTTP de type HEAD envoyées par le client pour déterminer la disponibilité des différents nœuds du cluster étaient donc rejetées.

L’appel au service de recherche Elasticsearch est également très simple. Dans l’attribut body de la fonction search proposée par l’API, est utilisé le formalisme standard de déclaration des requêtes au format JSON. En complément, les attributs index et type permettent respectivement d’indiquer sur quel index Elasticsearch et sur quel type de document lancer la recherche. Voici un exemple d’appel :

Extrait méthode fullTextSearch

.factory('searchService', ['es', function (es) {
        return {
            'fullTextSearch': function (from, size, text) {
                return es.search({
                    index: 'musicalbum',
                    type: 'album',
                    body: {
                        'from': from,
                        'size': size,
                        'query': {
                            'bool': {
                                'must': [
                                    {
                                        'fuzzy_like_this': {
                                            'fields': [
                                                'name',
                                                'artist.name',
                                                'year.string'
                                            ],
                                            'like_text': text,
                                            'min_similarity': 0.7,
                                            'prefix_length': 1
                                        }
                                    }
                                ]
                            }
                        },
                        'facets': {
                            'artist_type': {
                                'terms': {
                                    'field': 'artist.type_id'
                                }
                            },
                            'album_rating': {
                                'histogram': {
                                    'key_field': 'rating.score',
                                    'interval': 21
                                }
                            },
                            'album_year': {
                                'range': {
                                    'field': 'year',
                                    'ranges': [
                                        { 'to': 1970},
                                        {  'from': 1970, 'to': 1980},
                                        {  'from': 1980, 'to': 1990},
                                        {  'from': 1990, 'to': 2000},
                                        {  'from': 2000, 'to': 2010},
                                        {  'from': 2010 }
                                    ]
                                }
                            }
                        }
                    }
                });
            },

La fonction search renvoie une promesse de réponse. Pour récupérer la réponse retournée par Elasticsearch, la méthode then peut être utilisée :

searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
                function (resp) {
                    $scope.searchResp = resp;
                    $scope.totalItems = resp.hits.total;
searchService.fullTextSearch(from, $scope.pageSize.count, text).then(
    function (resp) {
        $scope.searchResp = resp;
        $scope.totalItems = resp.hits.total;
    }
);

Localisation

Le service $locale d’Angular permet de formater les nombres et les dates en fonction des préférences linguistiques de l’utilisateur. Il existe autant de fichiers JavaScript que de combinaisons langue / pays (exemples : angular-locale_fr-fr.js, angular-locale_en-us.js).

Pour charger le fichier adéquat, l’application doit détecter le langage défini par l’utilisateur dans son Navigateur. A première vue, les variables du DOM window.navigator.userLanguage et window.navigator.language auraient dû apporter cette information. Il en aurait été trop simple. L’article Detecting a Browser’s Language in Javascript explique précisément pourquoi.
Le header HTPP Accept-Language ne peut être lue que côté serveur web. Or, l’application était jusque-là full JavaScript. Convertir la page index.html en une page PHP ou JSP aurait été simple. Néanmoins, j’ai préféré m’affranchir de toute installation côté serveur. J’ai donc utilisé le service http://ajaxhttpheaders.appspot.com mis à disposition sur Google App Engine et dont voici un exemple d’utilisation :

$http.jsonp('http://ajaxhttpheaders.appspot.com?callback=JSON_CALLBACK').
success(function (data) {
    var acceptLang = data['Accept-Language'];
    langRange = userLanguage.getFirstLanguageRange(acceptLang);
    language = userLanguage.getLanguage(langRange);
    if (sessionStorage) {
        sessionStorage.setItem('userLanguageRange', langRange);
    }
}).
finally(function () {
    loadI18nResources();
});

Une fois la langue de l’utilisateur connue, la fonction $.getScript de JQuery permet de charger dynamiquement le fichier JavaScript Angular correspondant.
Afin d’éviter des appels intempestifs au service http://ajaxhttpheaders.appspot.com, la langue est conservée dans le sessionStorage du navigateur.
Dans un souci d’internationalisation, l’application angular-musicbrainz a été traduite en 2 langues : le français et l’anglais. Les libellés affichés à l’écran dépendent donc des préférences utilisateurs. Le système mis en œuvre s’inspire de ce que proposent les articles Creating multilingual support using AngularJS et Traduction des libellés dans les vues AngularJS. Un objet translation contenant la traduction de tous les libellés d’une langue est chargé à partir d’un fichier JSON puis ai mis dans le scope parent ($rootScope). Cet objet peut être accédé à la fois dans les templates HTML que côté JavaScript :

<label id="search-input-label" class="col-sm-3 control-label" ng-bind="translation.SEARCH_LABEL">Searching a music album</label>
$scope.pageSizes = [
            {count: 5, label: '5 ' + $scope.translation.SEARCH_PAGE_RESULT},

Widgets graphiques

Le projet UI Bootstrap propose une douzaine de directives Angular basées sur Boostrap : sélection de date avec calendrier, accordéon, onglets, barres de progression, fenêtre popup, collapse, carrousel d’images …

Notre web app de recherche utilise 3  de ses directives :

DirectiveVisuelExemple d’utilisation dans les templates HTML
typeahead angular-musicbrainz-typeahead
<input type="text" 
class="form-control" 
ng-model="searchText" 
typeahead="album for album 
in autocomplete($viewValue) 
| filter:$viewValue" />
pagination angular-musicbrainz-pagination
<pagination total-items="totalItems" 
page="currentPage"
max-size="maxSize"
num-pages="numPages" 
items-per-page="pageSize.count" 
on-select-page="selectPage(page)">
</pagination>
pagerangular-musicbrainz-pager
<pager total-items="totalItems"
page="currentPage"
on-select-page="selectPage(page)">
</pager>

Comme le montre les exemples ci-dessus, les directives UI Boostrap permettent d’étendre le HTML, soit par de nouveaux tags (ex: <pagination/> , soit par des attributs enrichissants des tags standards (ex : typeahead sur <input/> ).

Tests unitaires

Avec son découpage en modules, la possibilité de créer des mocks et l’indépendance du code JavaScript au regard du DOM, Angular permet de tester unitairement chaque contrôleur, service, filtre, route et directive. Qui plus est, l’application blanche angular-seed  vient avec toute l’infrastructure de tests : spécifications Jasmine à compléter, configuration Karma, scripts batch et shell permettant d’exécuter les tests. Autant dire, le développeur n’a aucune excuse pour ne pas tester unitairement son application.
L’application angular-musicbrainz comptabilise 36 tests unitaires, couvrant ainsi 65% du code source. Avant de pouvoir exécuter les tests unitaires, il est nécessaire d’installer les quatre modules Karma suivants :

npm install -g karma karma-junit-reporter karma-ng-html2js-preprocessor karma-coverage

Les tests unitaires peuvent être exécutés de 2 manières :

  1. Par la commande grunt : grunt karma
  2. Ou par un script : scripts\test.bat

Karma exécute les tests puis se met en attente de changements. En effet, tel infinitest, Karma relance les tests à chaque modification du code source ou des tests. Cela s’avère très pratique pour lever au plus tôt toute régression ou bien travailler en TDD.

Autre aspect de Karma : il permet de faire tourner les tests simultanément dans un ou plusieurs navigateurs. Dans le fichier de configuration karma.conf.js, Google Chrome et le navigateur headless PhantomJS ont été retenus.

Une fois la structuration d’un cas de test prise en main (mots clés describe, beforeEach et it), l’écriture du code de tests est plus ou moins simple. La difficulté principale vient de la lourdeur de la configuration nécessaire à mettre en place pour bouchonner les adhérences. Voici par exemple comment tester la fonction fullTextSearch du contrôleur SearchCtrl :

it('fullTextSearch should put the searchResp variable into the scope', function () {

            expect(scope.searchResp).toBeUndefined();
            expect(scope.isAvailableResults()).toBeFalsy();
            expect(scope.isAtLeastOneResult()).toBeFalsy();

            scope.fullTextSearch('U2', 1);
it('fullTextSearch should put the searchResp variable into the scope', function () {

    expect(scope.searchResp).toBeUndefined();
    expect(scope.isAvailableResults()).toBeFalsy();
    expect(scope.isAtLeastOneResult()).toBeFalsy();

    scope.fullTextSearch('U2', 1);

    // scope.$digest() will fire watchers on current scope,
    // in short will run the callback function in the controller that will call anotherService.doSomething
    scope.$digest();

    expect(scope.searchResp).toBeDefined();
    expect(scope.totalItems).toBeDefined();
    expect(scope.isAvailableResults()).toBeTruthy();
    expect(scope.isAtLeastOneResult()).toBeTruthy();
});

Le contrôleur SearchCtrl  s’appuie sur le service searchService dont la fonction fullTextSearch a dû être bouchonnée. Au final, le développeur écrit plus de code de test que de code testé.
Espérons que le duo Karma / Jasmine gagnera en maturité avec le temps. En Java, l’utilisation des annotations @Mock et @InjectInto permet en effet de réduire drastiquement ce type code.

Non des moindre, le dernier point à connaître lors de l’écriture des tests concerne les assertions. Venant avec un nombre de matchers clés en mains, Jasmine permet d’écrire ses propres matchers.

Tests end-to-end

A l’instar de ce que propose Selenium dans le monde Java, Karma permet d’écrire et d’exécuter des scénarios fonctionnels. Un prérequis à leur exécution est que l’application web doit être démarrée.

Là encore, l’application blanche angular-seed  vient avec toute l’infrastructure de tests e2e nécessaire. Comme prérequis, le module karma-ng-scenario doit être installé via npm :

npm install -g karma-ng-scenario

Les tests e2e peuvent être exécutés en ligne de commande: scripts\e2e-test.bat

Pour l’écriture des scénarios de tests, le module angular-scenario fournit un DSL permettant de sélectionner des éléments du DOM et de simuler des évènements utilisateurs. A noter que le framework Protactor doit remplacer à termes ce module.
Comme le montre l’extrait de code ci-dessous, le code reste lisible :

describe('search', function () {

    beforeEach(function () {
        browser().navigateTo('#/search');
    });

    it('should render search when user navigates to /search', function () {
        expect(element('#search-input-label').text()).
        toContain('music');
    });

    it('U2 album search', function () {
        input('searchText').enter('U2');
        element(':button').click();
        expect(element('#result-number').text()).
        toContain('22');

    });

});

Créé par l’un des développeurs d’Angular, Karma a l’avantage de connaître le fonctionnement interne d’Angular. Cette faculté lui permet de résoudre les problèmes de requêtes Ajax souvent rencontrés dans les tests Selenium. Adieux les tempos ou autre  waitForElement.

angular-musicbrainz-e2e

Exécution des tests end-to-end dans Chrome :

Contrôle qualité avec JSHint

Fork actif de jslint, JSHint s’apparente au Checkstyle du monde Java. Cet outil Open Source effectue plusieurs types de vérifications sur les fichiers JavaScript :

  • Conventions de nommage
  • Règles de formatage
  • Bonnes pratiques permettant d’éviter de potentiels bugs

Le fichier de configuration .jshintrc permet d’activer chacune des dizaines de règles proposées par JSHint. Activée sur notre projet, la règle curly vérifie par exemple s’il ne manque pas des accolades dans les boucles et les conditions.

La vérification des fichiers JavaScript peut ensuite se faire, soit en ligne de commande :

D:\Dev\angular-musicbrainz>grunt jshint
Running "jshint:all" (jshint) task
Linting app/js/services.js ...ERROR
[L140:C17] W116: Expected '{' and instead saw 'return'.
return undefined;

Soit directement depuis IntelliJ IDEA après configuration :

angular-musicbrainz-jshint
angular-musicbrainz-jshint2

JSHint a toute sa place sur un projet de grande taille sur lesquels de nombreux développeurs travaillent puis se relaieront pour sa maintenance. Sur de plus modestes applications comme angular-musicbainz , il a le mérite de former et de mettre en garde des développeurs JavaScript Junior.

Usine de dév JavaScript

Dans le billet Ma petite usine logicielle, je vous expliquais comment utiliser CloudBees et GitHub pour industrialiser vos projets Java. Vous l’aviez compris, l’intégration continue et l’automatisation des tâches me tiennent à cœur. J’ai donc naturellement regardé ce qui existait dans le monde JavaScript. Ce dernier n’est pas en reste. Voici ce que j’ai mis en place sur angular-musicbrainz.

Déjà mis en place sur mes projets Java avec Maven, Travis CI est une plateforme d’intégration continue mise à disposition gratuitement pour les projets Open Source. Cette plateforme présente l’avantage de supporter NodeJS et peut donc intégrer des applications JavaScript.

La configuration du build Travis se trouve dans le fichier .travis.yml :

language: node_js
node_js:
  - 0.10

before_script:
  - npm install -g grunt-cli

script:
  - grunt karma:ci

after_success:
  - grunt coverage

On demande à Travis d’installer le client Grunt avant d’exécuter les tests unitaires et de publier la couverture de code. A chaque commit dans le repo GitHub, le build est lancé. La sortie console s’affiche en temps réel :

angular-musicbrainz-travis


En cas d’échec du build, vous pouvez être notifiés par email , IRC, webhook …

Outre la génération d’un rapport de couverture de code testé, la commande grunt coverage  envoie ce rapport au service Coveralls. Ce dernier historise le taux de couverture et offre une IHM permettant de naviguer parmi les fichiers analysés.

L’historique des builds du projet angular-musicbrainz est accessible en ligne :

angular-musicbrainz-coveralls1


Le taux de couverture du build n°494581 est également consultable en ligne :

angular-musicbrainz-coveralls1


Autre service en ligne intéressant : pouvoir vérifier rapidement que les dépendances d’une application sont à jour. C’est ce que propose David. Voici visuellement la synthèse proposée par David pour les dépendances de dev d’angular-musicbainz :

angular-musicbrainz-david

A noter qu’un service similaire pour les dépendances utilisées par Bower serait intéressant.

Chacun de ces services propose un badge dynamique. Pratique, ces badges peuvent être affichés dans le README.MD :

angular-musicbrainz-build-status
Conclusion

Ce long billet m’aura permis de vous faire découvrir les différentes facettes du monde JavaScript dont j’ai fait connaissance tout au long du développement de cette petite application web de recherche.
L’utilisation d’Angular est plaisante et me réconcilie avec le développement JavaScript que je ne trouvais jusque-là pas assez industrialisé. Diminuant le nombre de lignes de code JavaScript au profit du HTML, ce framework permet de structurer proprement le code JavaScript. Ceux qui ont connus des applications où chaque page comporte des centaines de lignes jQuery non organisées apprécieront sans aucun doute.

En quelques années, je constate avec plaisir que l’écosystème JavaScript a rattrapé son retard sur celui de Java : intégration continue, outils de builds, tests unitaires, tests fonctionnels, qualimétrie,  gestion des dépendances, MVC, data-binding, POJO, templating, injection de dépendances, modularisation, nombre grandissant de frameworks, moteur d’exécution optimisé, support IDE … Chapeau bas.

]]>
AngularJS à Devoxx France 2013 https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/ https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/#respond Tue, 02 Apr 2013 16:44:53 +0000 http://javaetmoi.com/?p=667 Continuer la lecture de AngularJS à Devoxx France 2013 ]]> Université AngularJS ou le futur du développement Web à devoxxx France 2013

A deux semaines de sa première formation en entreprise sur AngularJS, répétition générale pour Thierry Chatel devant 200 développeurs avides d’en apprendre un peu plus sur le dernier né des frameworks JavaScript de Google. Développeur Java / Swing chez IBM au début des années 2000, Thierry s’est ensuite dirigé vers du conseil en architecture avant de découvrir AngularJS durant l’été 2011. Depuis, il y consacre beaucoup d’énergie et anime notamment le site FrAngular.com, premier blog francophone dédié à ce framework.
Assez parlé de sa personne, lui-même n’en serait que trop gêné.

Comme de nombreux développeurs venus assister à cette conférence, j’étais curieux de découvrir à mon tour le framework qui avait fait autant parlé de lui lors de Devoxx World 2012. Et autant vous l’annoncer dès maintenant : je n’ai pas été déçu.

D’une durée de 3h, cette Université intitulée AngularJS, ou le futur du développement Web s’est décomposée en une première partie théorique suivie d’une seconde plus pratique basée sur différents types d’applications : Last Tweets, directive Google Maps et Game Store. Live coding et démos furent au rendez-vous.
Pour la petite anecdote, les slides de la présentation sont écrits avec la syntaxe Markdown et sont interprétés par l’outil Angular Showoff reposant, vous l’aurez deviné, sur Angular. L’intérêt majeur est qu’ils peuvent embarquer du code Angular, pratique pour les démos in-slides telles que :

Your name: 
Hello {{me}}!

La fonctionnalité clé

La fonctionnalité phare que recherchait notre speaker dans un framework JavaScript était le data binding bi-directionnel entre le modèle et les vues. C’est donc ce qu’il a particulièrement apprécié dans Angular et mis en avant dès le début de sa présentation.
Par data binding bi-directionnel, on entend qu’une mise à jour du modèle JavaScript soit répercutée sur l’IHM et, inversement, qu’une interaction de l’utilisateur (ex : saisie dans un champ texte) soit aussitôt reflétée dans le modèle. C’est le comportement que l’on retrouve typiquement côté serveur avec des technologies telles JSF entre la vue xhtml et les beans managés.
La majorité de ses concurrents JavaScript n’assurent le binding que dans un seul sens : des données du modèle vers la vue HTML. En général, l’opération inverse doit être implémentée manuellement en JavaScript en s’abonnement aux évènements du DOM, par exemple à l’aide de JQuery.
Angular permet de gérer nativement ce binding qui, jusque-là, faisait défaut au HTML. Concrètement, cela se traduit par drastiquement moins de code JavaScript de manipulation du DOM et, implicitement, une compréhension du code et une maintenance facilitée. Je confirme que, dans les applications présentées lors de ce show, nous n’avons pas vu une ligne de ce type de code.
Autre aspect en faveur d’Angular : le modèle JavaScript ainsi que les différents services JS écrits par le développeur ne requièrent aucune adhérence avec Angular. On retrouve le côté POJO de Java et des beans Spring. C’est ce qui donne son petit côté magique au framework.

Le mécanisme

Le mécanisme sous-jacent au data binding est introduit par Thierry au travers du slogan d’Angular « HTML enhanced for webapps ». En effet, le parti pris d’AngularJS est d’étendre le HTML, de l’enrichir avec des directives Angular, à savoir des tags et des attributs supplémentaires ainsi que des expressions entre {{double accolades}}. La page HTML fait elle-même office de Template.
Au démarrage de l’application, lors d’une phase de compilation, le framework parcourt le DOM à la recherche de ces balises puis instrumente le code HTML afin d’assurer le binding.  A partir de ce point, modèles et vues sont synchronisés en continue.
La force d’Angular réside dans le fait qu’il n’est nul besoin d’utiliser JavaScript pour déclarer un template. C’est la page HTML qui est étendue pour jouer ce rôle.

Sous le capot d’Angular, la détection des changements côtés vue s’appuie sur des évènements du DOM (ex : onclick, onkeyup) ou extérieurs (ex : timeout, réponse Ajax).
Par contre, les modifications du modèle sont détectées par un mécanisme bien spécifique : le dirty checking. A ce que j’en ai compris, il permet de surveiller tout objet JavaScript, de détecter qu’une valeur a changé puis d’exécuter du code JS. J’entrevoie une similitude avec le fonctionnement d’Hibernate qui surveille les changements opérés sur les objets attachés à sa session, ceci pour les refléter en base de données.

Le dirty checking s’appuie sur des watches, des listeners à l’écoute des changements. Lors de la compilation du template, Angular positionne automatiquement des watches sur les expressions et les tags HTML bindés avec un modèle. Pour des problématiques de performance, Thierry nous alerte sur le fait que l’expression surveillée ne doit pas être trop couteuse à évaluer.
Pour les objets « non managés » par Angular (issus par exemple d’API tierces), le développeur peut déclarer des watches de manière impérative.  Cette possibilité a été illustrée lors du développement d’une directive Angular utilisant l’API Google Maps. Le but était de surveiller le déplacement du centre de la carte via la souris de l’utilisateur et, le cas échéants, de mettre à jour les 2 champs de saisie correspondant à la latitude et la longitude, champs par ailleurs modifiables au clavier.

Syntaxe utilisée pour ajouter un watcher :

scope.$watches('center', function () { map.setCenter(scope.center); }, true);

L’objet center possédant 2 propriétés, le paramètre booléen true précise à Angular qu’il doit surveiller l’objet en profondeur.

Pour terminer sur ce sujet, notre orateur nous laisse rêveur en nous annonçant que ce mécanisme de dirty-checking est à l’étude au sein du W3C sous le nom de Object.observe() et que certaines versions avant-gardistes de navigateurs telles Chrome Canari ou Chronium l’implémentent déjà nativement, multipliant ainsi par 20 les performances actuelles du dirty-checking d’Angular.

Les autres concepts

Bien entendu, la force d’Angular ne se limite pas au seul data binding bi-directionnel. Le framework met à disposition de nombreux concepts bien connus des développeurs Java :

  • MVC : le pattern Modèle Vue Contrôleur fait partie intégrante d’Angular. Le HTML enrichi représente la vue. Des actions positionnées sur la vue permettent de faire appel à des contrôleurs JavaScript. Ces derniers exécutent le code applicatif puis mettent les données à disposition de la vue par l’intermédiaire d’un contexte. Thierry nous explique que les contrôleurs peuvent fonctionner sans la vue, ce qui est intéressant pour les tests. Une mauvaise pratique est donc de manipuler le DOM dans le contrôleur. Il nous rappelle aussi que le pattern MVC permet d’avoir plusieurs vues pour les mêmes contrôleurs / données ; une vue pouvant ainsi être dédiée aux appareils mobiles.
  • Injection de dépendance : les amateurs de Spring, de Guice ou de CDI retrouveront avec plaisir ce pattern dans Angular. Lors de l’appel d’une méthode d’un contrôleur, Angular lui injecte en paramètre les services dont il a besoin. Ces services peuvent être fournis par Angular (ex : $http pour des appels REST) ou par le développer qui les aura préalablement configurés à l’aide d’un provider. Pour les curieux, Thierry rentre dans le détail de l’implémentation en expliquant qu’Angular se base sur la signature des méthodes pour déterminer le service à injecter. Le nom des paramètres est interprété comme le nom du service à injecter. Cette approche pose problème lorsque le code JS est minifié. Il devient alors nécessaire de déclarer explicitement le nom des services à injecter en passant par un tableau de chaînes de caractères.
  • Service : tout objet JS peut être mis à disposition du reste de l’application  en tant que service. Outre le fait de pouvoir mutualiser du code faisant, par exemple, appel à des services REST du backend, les services permettent de conserver des données entre 2 contrôleurs. Thierry prend l’exemple d’un panier d’achat issu de l’application Game Store.
  • Navigation : la navigation entre les différentes vues s’appuie sur le mécanisme de routes bien connu des développeurs PlayFramework. Boutons précédents, suivants et marque-pages sont gérés nativement.
  • Scope : en Java, que ce soit en JSF ou avec Spring, les beans ont une portée (un scope). Dans Angular, cette notion prend un autre sens. Il faut le voir comme un contexte d’exécution qui peut être propre à une directive Angular. Les scopes sont utilisés lors de l’évaluation des  {{ expressions }}. Lorsque l’objet référencé n’est pas présent dans le contexte courant, Angular va le chercher dans le contexte englobant, et ainsi de suite, en remontant la hiérarchie dans le DOM. Cela me fait penser aux EL de l’API Servlet qui utilisent dans l’ordre les portées page, request, session et application.
  • Filtre : dans Angular, les filtres ne sont pas à rapprocher des filtres JavaEE ou Spring Security, mais aux pipes Unix. Thierry a utilisé un filtre pour parser le texte d’un tweet et y ajouter une balise <a> lorsqu’une URL est détectée. Angular semble disposer d’un ensemble de filtres prêts à l’emploi.

Les tests

Lors de cette présentation, l’outillage concernant les tests n’a pas été oublié.
Pour les tests fonctionnels, Karma (ex-Testacular) permet d’exécuter des tests dans les navigateurs connectés à l’outil. Une démo a été réalisée sous nos yeux avec Chrome, Firefox, IE et depuis un Smartphone. Karma présente l’avantage d’utiliser le moteur JavaScript des différents navigateurs connectés.
Tel infinitest, Karma relance les tests à chaque sauvegarde du code.
Créé par Vojta Jína, développeur principal d’Angular, Karma connait le fonctionnement d’AngularJS. Cela lui permet de résoudre de manière transparente le problème des requêtes Ajax souvent rencontré dans les tests Selenium.

Du point de vue des tests unitaires, Angular permet de tester des contrôleurs et des services JS nécessitant de faire appel à un serveur. Pour se faire, Angular fourni le mock object _$httpBackend_ qui permet de simuler un appel au serveur et de retourner le jeu de données nécessaire au test (ex : au format JSON).

Enfin, dans le cadre d’un débogage, le plugin Batarang pour Chrome facilite l’inspection du DOM et des scopes.

La cible

D’après notre speaker, Angular cible avant tout les applications de gestion qui font habituellement recourt à de multiples champs de saisie.
Dédié aux applications mono-pages (SPA), il ne le recommande pas pour les sites web, d’autant plus qu’ils ne seraient pas indexables dans Google. Un paradoxe.
A noter qu’Angular est un framework qui s’utilise uniquement côté client, à savoir dans le navigateur, et qui n’a donc aucun sens à être embarqué côté serveur (ex : dans Node.JS).

Encore jeune, très peu d’applications visibles sur le Net reposent sur Angular. Thierry nous cite tout de même quelques exemples :

  • le nouveau manager d’OVH,
  • le site de Google « Doubleclick for publisher » dédié aux professionnels de la publicité,
  • et le site Youtube de la PS3.

Les limites

Thierry nous parle de la limite des 2000 watches. Limite que l’on peut facilement dépasser si l’on n’y prête pas attention et que l’on dispose d’une machine puissante. Ce seuil est d’autant plus valable sur les mobiles. Une astuce consiste à ne surveiller que les données visibles par l’utilisateur et à désactiver les watches sur les expressions qui ne varieront jamais.

Le framework Angular ne dispose pas de composants UI. Il permet de réutiliser des composants JQuery UI, mais un adaptateur est nécessaire. Des librairies externes telles qu’AngularUI ou AngularStrap commencent à voir le jour.

Par ailleurs, en tant que framework, Angular est particulièrement structurant. Simple librairie, JQuery l’est beaucoup moins. Une application basée sur Angular doit donc adhérer complètement à sa philosophie ; il n’est pas intéressant de n’utiliser que certaines briques. A noter tout de même qu’il est possible de n’utiliser Angular que sur une partie de son application web et de le positionner, par exemple, que sur un simple div.

Lors de la série des questions/réponses, un développeur de l’assistance fait remarquer qu’Angular demande une syntaxe spécifique pour fonctionner sous IE 7. Son fonctionnement sur d’anciens navigateurs semble donc nécessiter quelques adaptations.

Lors de cette session, certaines fonctionnalités indispensables à toute IHM n’ont pas été abordées. C’est par exemple le cas de la validation des données saisies dans les formulaires.

Conclusion

Malgré sa jeunesse, AngularJS semble chambouler le foisonnant écosystème des frameworks JavaScript (Backbone.js, Knockout, Ember.js). Certains le voient déjà comme l’inspirateur d’ HTML 6. Avec DART, GWT et Angular, Google possède plusieurs poulains. Reste à savoir lequel sera le plus endurant ?

Pour ma part, outre l’omniprésent JQuery, je n’avais encore jamais pris le temps d’étudier l’un de ces frameworks JavaScript qui détrônent progressivement les technologies côté serveur.
Cette université m’aura conforté dans mon choix de regarder de plus près ce que propose AngularJS.
D’après Thierry, apprendre correctement le JavaScript est un pré-requis. L’apprécier en est un second. Avant d’aborder les tutoriaux sur Angular, je vais donc commencer pas ressortir le livre JavaScript, gardez le meilleur de Douglas Crockford.

Références :

]]>
https://javaetmoi.com/2013/04/angularjs-devoxx-france-2013/feed/ 0