Introduction à AngularJS

Après notre présentation générale du contexte, il est temps d’aborder AngularJS plus concrètement. Voici donc une introduction au framework orientée vers les aspects les plus pratiques pour pouvoir démarrer.

Installation

Pour se familiariser avec AngularJS, le mieux est de commencer par le tutorial, plutôt bien fait.

Si l’on veut développer une application « from scratch », l’application prototype est sans doute un bon point de départ.

Dans tous les cas de figure, il va falloir installer NodeJS, puisqu’il embarque différents outils utiles, notamment npm, lequel permettra d’installer Bower (gestionnaire de modules), Karma (tests unitaires), Protractor (tests applicatifs) et même un serveur HTTP destiné au développement.

Se reporter au tutorial pour l’installation de tout cela.

A noter que si l’on décide d’utiliser une application JEE comme dorsal, on n’utilisera évidemment pas le serveur HTTP de développement.

Il est possible (et préférable) d’utiliser un IDE, comme Eclipse, pour développer des applications AngularJS (on peut aussi utiliser IntelliJ IDEA ou Netbeans, suivant les habitudes du développeur).

Sous Eclipse, on doit installer le plug-in ad hoc, en suivant les instructions données ici.

Le plug-in prend en charge la complétion et propose une vue spéciale Angular des ressources utiles.
Toutefois, sa valeur ajoutée est assez limitée. Chaque nouvelle ressource/nouveau script doit être déclaré manuellement pour être pris en compte par le plug-in. Il est aussi possible d’activer en même temps la nature Javascript d’Eclipse qui dans certains cas assure mieux la complétion.

Principes de base d’AngularJS

Avant toute chose, il faut admettre que la documentation AngularJS est parfois nébuleuse, et que commencer par le tutorial est une chose presque indispensable si l’on veut y comprendre quelque chose.

AngularJS est donc un framework MVC (Google appelle ça MVW, mais c’est globalement la même chose). On a donc :

  • Un/des modèle(s)
  • Des vues (en pratique du HTML étendu)
  • Des contrôleurs (écrits en Javascript)

Il faut ajouter à cela les notions suivantes :

  • Des directives (au sens AngularJS), c’est à dire la possibilité de créer de nouveaux objets HTML/DOM. A noter que les directives ne seront pas abordées dans cet article. Aussi lorsque par la suite on emploiera le terme « directive« , il se s’agira pas de directive au sens d’AngularJS.
  • Un binding bidirectionnel
  • L’injection de dépendances
  • Des services, au sens le plus large du terme. En particulier $resource qui permet de dialoguer en JSON avec une application distante est un service. On peut évidemment créer ses propres services. On peut voir le service comme un bout de programme Javascript qui fournit un service quel qu’il soit ; dans sa philosophie, il est assez proche du service Java dans une approche SOA mais il peut aussi s’agir d’un utilitaire d’un autre niveau.
  • Un système modulaire : en pratique, les services et les contrôleurs sont des modules. Les add-ons tierces (gérés par Bower) sont aussi des modules.
  • Un système de routing qui permet de passer d’une page (ou d’un segment de page) à l’autre en fonction de la nature de l’url.
  • Par défaut, aucune bibliothèque de composants graphiques ni de framework CSS n’est fourni. Bootstrap est alors un bon complément (d’autant qu’il existe une extension AngularJS basée dessus). Pour l’utiliser, il vous faudra donc installer Bootstrap via Bower.

D’un point de vue de l’organisation du code, une application AngularJS se présente généralement comme ceci :

  • à la racine se trouve la page HTML principale index.html)
  • bower_components/ : c’est là que se trouvent les modules additionnels téléchargés/gérés par Bower
  • js/ : c’est là que se trouvent les programmes Javascript en particulier les contrôleurs et les services.
  • partials/ : c’est là que se trouvent les vues HTML (portions de la vue principale index.html)

Quelques remarques avant de commencer

Un des gros points noirs d’AngularJS vient de ce que le client plante sans prévenir s’il y a une erreur de syntaxe dans un contrôleur et/ou un service; visuellement, cela se manifeste par un écran blanc. Il est donc vivement conseillé d’activer la console web du navigateur (Firefox ou Chrome/Chromium conseillés) pour détecter ces erreurs. Malheureusement, même comme cela, l’erreur est assez peu explicite.

Dans le même ordre d’idée, il est parfois nécessaire d’utiliser le debugger JS du navigateur mais il ne faut pas oublier que les données sont souvent remplies de façon asynchrone, ce qui ne facilite pas le debuggage.

Pour que les modifications de code soient prises en compte, il est nécessaire de recharger la page depuis le navigateur sur le partial associé (du moins avec Firefox).

Commencer par le début

Une application AngularJS est une application. C’est quelque chose qu’il ne faut pas perdre de vue : il ne s’agit plus de pages HTML générées par un serveur (comme avec Struts), mais d’une logique intégralement chargée dans le navigateur dès le lancement du programme. Se l’imaginer comme une application desktop est la meilleure analogie que l’on puisse faire.

Les best practices d’AngularJS encouragent à ce que l’on déclare cette application dans le module app.js (dans le répertoire js, donc). Ce fichier est le point d’entrée pour le code de l’application.

Voici une déclaration de base :

/* App Module */
var myApp=angular.module('myApp',[ 'ngRoute', 'myControllers', 
                                    'myServices' ]);

On déclare donc une application en la nommant (myApp), et on liste les modules qu’elle utilise (ici la table de routage d’Angular, les contrôleurs et les services de l’application elle-même). Il s’agit là d’une déclaration minimale ; si l’on voulait par exemple utiliser le module qui gère les cookies, il faudrait ajouter ngCookies.

A noter quand même une chose un peu troublante : cette déclaration n’est pas suffisante pour inclure un module. Pour que tout cela fonctionne, il faut en plus déclarer l’import du code javascript correspondant dans la vue principale (index.html).

Exemple :

<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js">
</script> 
<script src="js/app.js"></script> 
<script src="js/controllers.js"></script> 
<script src="js/services.js"></script>

On retrouve là :

  • L’application elle-même (app.js)
  • La table de routage (angular-route.js)
  • Les contrôleurs (controllers.js)
  • >Les services (services.js)
  • >Et évidemment AngularJS lui-même (angular.js)

A noter aussi que Bootstrap n’est pas un module Angular et n’est donc pas déclaré dans app.js. Il est juste inclus en tant que CSS (et éventuellement javascript) dans index.html.

app.js sert aussi à définir la configuration de l’application. On peut par exemple y définir des interceptors (des handlers sur certains événements) ou d’autres choses encore. Mais en ce qui nous concerne, dans le cas d’une application minimale, la seule chose à y ajouter est la table de routage

Un exemple :

petstoreApp.config(['$routeProvider', 
  function($routeProvider) {
     $routeProvider.       
       when('/accueil', {
         templateUrl: 'partials/accueil.html',
         controller: 'InitCtrl'
       }).
       when('/category/:categoryId', {
         templateUrl: 'partials/category.html',
         controller: 'CategoryCtrl'
       }).
       otherwise({
         redirectTo: '/accueil'
       });
 }]);

On voit ici qu’en fonction de l’url en entrée, certaines vues (partials) vont être affichées. De plus, pour chaque partial est défini le contrôleur qui lui est associé ce qui est assez normal puisqu’on est dans une logique MVC.

A noter que dans l’exemple, par défaut (otherwise) on affiche /accueil qui reboucle sur la table elle-même et envoie sur partials/accueil.

Vues et modèles

Les vues d’AngularJS sont tout simplement des pages HTML enrichies d’expressions dans des accolades ({{…}}) faisant le lien avec le modèle et de directives qui sont des balises ou des paramètres ajoutés spécifiquement pour apporter du comportement. Il existe notamment un ensemble de directives prédéfinies par Angular reconnaissables par le préfixe « ng- ». La directive ng-model permet également de faire le lien avec le modèle. On verra plus loin que le modèle, en pratique, est stocké dans la variable $scope du contrôleur associé (par la table de routage, voir plus haut).

Un exemple pour l’affichage d’une liste d’objets :

<table class="table table-bordered">
   <thead>
      <tr> 
      <th>Item ID</th>
      <th>Product</th>
      <th>Description</th>
      <th>Unit cost</th>
      </tr>
   </thead> 
   <tbody> 
      <tr ng-repeat="item in cart.items">
      <td><a href="#/item/{{item.itemId}}">{{item.itemId}}</a></td>
      <td>{{item.product.name}}</td>
      <td>{{item.description}}</td>
      <td>{{item.unitCost}}</td>
      </tr>
   </tbody>
</table>

Ici, grâce à la directive ng-repeat on boucle simplement sur une liste (contenue dans cart.items) et pour chaque ligne on affiche des champs de l’objet item.

Mais avant d’aller plus loin, il faut se demander ce qu’est la notion de partial.

Dans une application web, on a en général, une partie fixe et une partie variable. La partie fixe peut être, par exemple, un bandeau en haut et un menu à gauche, le reste étant variable (c’est la partie qui « bouge »). A l’instar de Tiles avec Struts, Angular apporte un système de templating pour gérer ce cas de figure. Les partials sont simplement les vues qui peuvent prendre place dans la partie variable.

Si on regarde la page principale (index.html), on a quelque chose du genre :

<!doctype html>
<html lang="en" ng-app="myApp">
   <head>
   <!-- Inclusion des CSS et des scripts JS -->
   </head>
   <body ng-controller="MainCtrl">
      <div class="bandeau"> .... </div>
      <div class="menu-gauche">....</div>
      <div ng-view></div>
   </body>
</html>

La partie variable est ici taguée avec la directive ng-view : c’est là que s’afficheront les partials suivant les ordres de la table de routage.

On remarque aussi deux choses :

  • La directive ng-app qui définit l’application au niveau de la page principale.
  • La directive ng-controller qui définit le contrôleur pour la page principale (puisqu’on ne peut pas le faire au niveau de la table de routage). A noter qu’on peut aussi définir ainsi le contrôleur au niveau d’un partial, mais il est préférable de le faire au niveau de la table de routage.

Nous n’allons pas voir en détail toutes les directives ng-xxx, l’API AngularJS étant assez explicite et claire en ce qui les concerne.

Ce qui est plus important à ce stade, c’est la notion de binding.

<input ng-model="customer.address.city"/>

Dans l’exemple ci-dessus, on a un champ d’édition qui affiche la ville où habite un utilisateur. Il y a binding (bidirectionnel) entre la vue et le modèle. Cela signifie que lorsque le modèle est modifié (de l’extérieur par exemple du fait d’une requête asynchrone), la vue sera automatiquement mise à jour avec les nouvelles valeurs du modèle. Et réciproquement : si l’on change la valeur dans la vue (ici dans le champ d’édition), le modèle sera automatiquement mis à jour avec cette nouvelle valeur. Ce mécanisme très puissant doit être bien compris : en particulier, il n’est pas nécessaire que le modèle soit rempli pour qu’on puisse se référer à lui dans une vue. On verra plus loin dans le cas des requêtes asynchrones avec le serveur que le modèle n’est pas immédiatement mis à jour avec les données remontées du serveur (puisque les requêtes sont asynchrones). Juste après l’appel à la requête, le modèle ne contient qu’une promise, en pratique un objet vide qui sera rempli au retour de la requête. Toute la beauté du binding, c’est que lorsque la promise sera enfin chargée, les valeurs ad hoc apparaîtront dans la vue. Il est donc inutile de savoir ce qu’il y a dans le modèle (ou même s’il y a quelque chose) au moment ou l’on décrit la vue.

Il s’agit donc d’un mécanisme assez troublant de prime abord, mais qu’il faut bien assimiler, car c’est – entre autres – ce qui fait la puissance d’AngularJS.

Le Contrôleur

A chaque partial est donc associé un contrôleur. Pour une application simple, on regroupe en général, tous les contrôleurs dans un seul fichier (par exemple, controllers.js), mais on peut évidemment avoir plusieurs fichiers de contrôleurs, auquel cas il faut tous les déclarer comme modules dans app.js et les inclure dans index.html.

Un fichier de contrôleurs ressemble à quelque chose comme ça :

'use strict';

/* Controllers */

var varControllers=angular.module('myControllers',[]); 
varControllers.controller('FirstCtrl',['$scope','$routeParams',
                                  function($scope,$routeParams){
                                   ... 
                                   }]); 

varControllers.controller('SecondCtrl',['$scope','$routeParams',
                    'myService','$log',  
                   function($scope,$routeParams,myService,$log){
                      ... 
                   }]);
....
...

On a donc une liste de contrôleurs, chacun ayant été associé à un partial dans app.js (usage de $routeProvider) ou directement au niveau des vues (cas de index.html).

On définit en première ligne ce fichier de contrôleurs comme un module (nommé ici myControllers), et on instancie chaque contrôleur particulier via la variable varControllers. A noter que les contrôleurs, comme tous les modules, sont instanciés au démarrage de l’application.

On remarque la signature « doublée » d’un contrôleur. Ceci est rendu nécessaire dans le cas où le fichier des contrôleurs serait minifié (compactage du code javascript). Même si vous ne comptez pas minifier votre fichier, il est préférable d’utiliser cette convention d’écriture, même si elle est un peu laborieuse.

Toute la puissance de l’injection de dépendance apparaît ici : du moment qu’un module a été déclaré dans app.js (ou fait partie du noyau d’AngularJS), il peut être passé en paramètre à un contrôleur. En pratique, on passe toujours $scope (comme on le verra par la suite), et presque toujours $routeParams qui contient les paramètres de l’url qui a permis d’arriver au partial auquel est associé le contrôleur.

Dans le second contrôleur, on passe en plus $log (système de logs inclus dans AngularJS) et un service défini par l’utilisateur, myService. Au passage, on remarque que les modules internes à AngularJS (ou à ses extensions) commencent toujours par un ‘$’.

Et le modèle dans tout cela ? Et bien en pratique, il est contenu dans $scope, qui comme son nom l’indique est un scope (contexte) pour le partial associé. Il est d’ailleurs possible d’accéder au scope d’un autre partial : dans le cas qui nous occupe, on peut, par exemple, accéder au scope de index.html depuis un partial via $scope.$parent.

Prenons un exemple simplissime. Soit le contrôleur suivant :

varControllers.controller('SimpleCtrl',['$scope','$routeParams',
                                  function($scope,$routeParams){
                                       $scope.name = "John Doe";
                                  }]);

Si le partial associé est

<h1>Hello {{name}}</h1>

On verra apparaître à l’écran « Hello John Doe ».

Le $scope peut aussi contenir des fonctions qui sont généralement appelées lorsqu’on clique sur un lien ou un bouton (mais pas nécessairement). Imaginons qu’on veuille effectuer une redirection dans un contrôleur. Déjà, il faut passer le service $location en paramètre au contrôleur. Dans le code du contrôleur, on a quelque chose du genre :

$scope.doRedirect = function() {
    if(something) {
        $location.path("/url1");
    } else {
       $location.path("/url2");
    }
};

La table de redirection se chargera d’afficher le bon partial en fonction de l’url définie par la redirection (url1 ou url2).

Dans le partial associé au contrôleur, on aura par exemple :

<button ng-click="doRedirect()">Click to redirect</button>

En pratique, le $scope est généralement rempli via un ou des services, c’est ce que nous verrons bientôt : le prochain article traitera de la notion de service et évoquera la communication avec le serveur.

Trucs et astuces , , , ,