Accueil > Java EE > Premiers pas avec Angular JS

Premiers pas avec Angular JS

Cet article, proposé par Henri Darmet, fait suite à l’article A la découverte d’Angular JS qui présentait, de façon très large, les principes et la philosophie d’Angular. Dans ce second épisode, il sera question d’installer un projet Angular et d’expliquer au travers d’une micro-réalisation quels en sont les mécanismes fondamentaux. Ce projet s’inscrira dans un projet Java Web classique : cela nous permettra de toucher du doigt les problèmes d’intégration qui apparaissent dans cette architecture somme toute très classique.

Passons rapidement sur la définition du projet avec Maven ou pas, Bower ou pas, Grunt ou pas (outils de gestion des dépendances Java et Javascript et de construction de l’application finale) et installons tout à la main (c’est l’affaire de dix minutes) :

  • Il faut monter une application Java/Web classique (pour les utilisateurs de Maven, faites le nécessaire pour récupérer le moteur de WebService/REST CXF et le JAXB provider « Jackson », on en aura besoin plus tard).
  • Il faut télécharger Angular JS sur le site d’angular (http://angularjs.org/). C’est un zip de 4 méga, décompressé en 7 que vous dézippez dans la partie ressource (WEB-CONTENT) de votre application. Changez les noms des répertoires intermédiaires (qui portent le numéro de la version) si vous ne voulez pas devoir faire le tour de vos fichiers HTML chaque fois que vous montez de version.

Voilà, c’est prêt. Si, si, c’est prêt. Cool, non ?

On aurait même pu faire plus vite en utilisant les scripts mis en ligne par Google. Dans ce cas, il n’y a rien à télécharger, il suffit juste, dans vos fichiers HTML, de faire référence à ces scripts via internet. Et avoir internet bien sûr.

A l’heure où l’article est écrit, la dernière version stable est la 1.0.7. Une version 1.2 RC est proposée. Pour ne pas multiplier les risques, restons sur la 1.0.7.

Développons.

La sagesse en pareil cas est de commencer en reproduisant un petit truc censé marcher. La sagesse est une vertu cardinale en développement Javascript, indispensable à tout espoir de réussite. L’exemple initial du tutoriel fera très bien l’affaire. Cela consiste :

  • à écrire un template : dojo.html
  • à écrire un script définissant le controller (controllers.js)

J’avoue un courage très modeste. Je me suis contenté de faire copier/coller à partir du tutoriel de Google. Examinons ces artefacts, en commençant par le template :

<html ng-app>
<head>
<script src="libjs/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones">
{{phone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
</body>
</html>

Il ne faut pas être grand clerc pour en comprendre la finalité : il s’agit d’afficher une liste de descriptions de téléphones, chacun d’eux étant caractérisé par deux informations : son nom et sa snippet (?). C’est du html classique avec deux-trois bizarreries :

  • ng-app dans la balise <html>,
  • ng-controller dans la balise <body>,
  • ng-repeat dans la balise <li>
  • {{phone.name}} et {{phone.snippet}} dans le corps de la balise <li>

Toutes ces bizarreries, évidemment, ont Angular comme origine.

  • ng-app indique à partir d’où Angular doit prendre la main. En dehors de la balise sur laquelle ng-app est posé, Angular n’intervient pas. En dedans, le framework prend la main et ce n’est pas de la cosmétique !
  • ng-controller indique, pour une balise donnée et son contenu, quel objet controller sera utilisé. Ici, il n’y en aura qu’un seul, d’où son accrochage à la balise body. Le nom du controller (sa classe, en fait, ou ce qui, en Javascript, fait office de classe) est « PhoneListCtrl ». Retenez ce nom, il va vite réapparaitre.
  • ng-repeat indique qu’on va « itérer » sur cet élément et donc inclure dans la page autant de lignes <li> qu’il y a d’objets dans la mystérieuse variable « phones ». Notons que le téléphone d’une ligne est connu au sein de la ligne, à l’aide de la variable « phone ». C’est ng-repeat lui-même qui définit cette variable très locale.
  • Les expressions {{phone.name}} et {{phone.snippet}} indiquent qu’à cet endroit de la page, il faut lier (« bind ») le contenu avec les valeurs des attributs name et snippet de l’objet désigné par phone. Cette ligature implique que si l’objet est modifié, les mises à jour doivent être instantanément reportées au sein de la page.

Ceux qui ont utilisé JSF ne doivent pas se sentir vraiment dépaysés : la logique, vue de l’extérieur est la même.

Il reste cependant deux points à éclairer :

  • A quoi peut bien servir le controller PhoneListCtrl et comment peut-on le définir ?
  • Comment définit-on la liste des téléphones référencée par la toujours mystérieuse variable « phones » ?

La réponse à toutes ces questions est contenue dans le script Controller.js :

function PhoneListCtrl($scope) {
$scope.phones = [
{"name": "Nexus S",
"snippet": "Fast just got faster with Nexus S."},
{"name": "Motorola XOOM™ with Wi-Fi",
"snippet": "The Next, Next Generation tablet."},
{"name": "MOTOROLA XOOM™",
"snippet": "The Next, Next Generation tablet."}
];
}

Le controller Angular est en fait un monteur : il construit un « Presenter » qui servira à animer la vue. La vue ne s’adressera jamais au controller (l’objet PhoneListCtr), elle s’adressera à son objet scope (référencé dans le controller par $scope) et c’est scope qui fournira les données demandées. C’est scope, aussi, qui exécutera les méthodes qui doivent être déclenchées quand on sélectionne une ligne, appuie sur un bouton ou modifie la valeur d’un champ. Pour l’instant, nous n’avons pas besoin de grand-chose : juste la liste des téléphones. Définissons donc un tableau JSON qui les décrit et référençons le, par une variable appelée « phones » (la voilà notre mystérieuse variable) de l’objet scope.

Les plus sagaces l’auront compris : dans le template, toute expression Angular fait référence implicitement à scope. Ainsi, la répétition « phone in phones » doit être comprise comme « $scope.phone in $scope.phones » et « {{phone.name}} » doit être comprise comme « {{$scope.phone.name}} ».

Les puristes diront que je pêche par excès de vulgarisation. Ils auront raison. En fait il existe toute une hiérarchie de scopes qui héritent entre eux. Hériter veux dire que si une méthode ou une variable d’un scope donné n’est pas trouvée, la recherche se poursuivra sur le scope parent ($scope.$parent) et ainsi de suite, de façon totalement transparente, bien sûr. Au début était le rootScope, défini systématiquement par Angular. Quand nous entrons dans la partie « contrôlée » par PhoneListCtrl, un nouveau scope est créé héritant de RootScope. C’est ce scope dont il est question dans le bout de code que je viens juste d’exposer.

Mais d’autres scope sont créés : par exemple, un scope est monté pour chaque ligne créée par le ng-repeat. La variable « phone » est donc à chaque fois isolée. Notons que dans tous ces sous-scopes, la liste des téléphones est toujours accessible puisque le scope associé à chaque ligne hérite du scope construit par le controller.

Autre point : nous avons ici un premier exemple d’injection de dépendance Angular : le passage en paramètre du scope au contrôleur. Je ne m’étendrai pas ici sur ce mécanisme car l’exemple est tellement trivial qu’il n’est pas facile de voir qu’il s’agit en fait d’une injection. Quand nous aurons besoin de services métier ou de composants externes, l’écriture se complexifiera un peu et le mécanisme apparaîtra avec plus de clarté. Notons juste le point.

On appelle la page HTML et …

Ça marche ! Chez moi, tout a marché du premier coup. Je ne savais pas que je mangeais mon pain blanc !

Que s’est-il passé sous le capot ? Angular a pris la main dès que la page a terminé de se charger. Il a alors cherché sur l’arborescence DOM où se trouvait son point d’ancrage (le fameux ng-app). A partir de ce point, il est descendu dans l’arborescence à la recherche des balises (il n’y en a pas dans notre cas), des  propriétés (ng-controller, ng-repeat) et des inclusions {{}} Angular. Il a alors modifié l’arbre DOM, accroché les méthodes Javascript nécessaires, etc. Bref, il a modifié en profondeur le modèle objet de la page. Ce processus est appelé « compilation » Angular. A partir de ce moment, notre page est sous contrôle, elle peut s’exécuter.

J’entends déjà les plus circonspects dire : « voici le tutoriel le plus inutile de l’année, c’est un tutoriel à partir d’un autre tutoriel, officiel qui plus est ! »

D’accord. Apportons un peu de valeur ajoutée. Divergeons de l’exemple donné par Google. Ce que nous allons faire maintenant, c’est une première ébauche de « grille et détail ». L’idée est simple : en cliquant sur une ligne, nous allons la sélectionner. Le téléphone concerné apparaîtra au-dessous, sous la forme de deux champs de saisie (texte pour le champ nom, textarea pour le champ snippet).

Pour y arriver, que faut-il faire ? Presque rien…

  1. Ajouter dans notre template, le formulaire d’édition du téléphone sélectionné (connu sous le nom de la mystérieuse variable « editedPhone »). Notez l’utilisation de la propriété ng-model pour lier une propriété de l’objet téléphone à un objet <input> et <textarea>.
  2. Ajouter dans notre template, chaque fois que l’on clique sur une ligne,  l’appel au réflexe « select » qui prend le téléphone courant en paramètre (le « phone » de « phone in phones »). Il existe une propriété Angular qui fait très bien l’affaire : « ng-click ».
  3. Définir ce réflexe dans le controller de sorte qu’il fasse référencer le téléphone courant par la variable « editedPhone ».

Ce qui donne, pour le template (dojo.html) :

<html ng-app>
<head>
<script src="libjs/angular/angular.js"></script>
<script src="js/controllers.js"></script>
</head>
<body ng-controller="PhoneListCtrl">
<ul>
<li ng-repeat="phone in phones" ng-click="select(phone)">
{{phone.name}} {{editedPhone.name}}
<p>{{phone.snippet}}</p>
</li>
</ul>
<div>
<div>
Nom : <input type="text" ng-model="editedPhone.name"></input>
</div>
<div>
Snippet :<textarea ng-model="editedPhone.snippet"></textarea>
</div>
</div>
</body>
</html>

Et pour le controller (controllers.html) :

function PhoneListCtrl($scope) {
$scope.phones = [ {
"name" : "Nexus S",
"snippet" : "Fast just got faster with Nexus S."
}, {
"name" : "Motorola XOOM™ with Wi-Fi",
"snippet" : "The Next, Next Generation tablet."
}, {
"name" : "MOTOROLA XOOM™",
"snippet" : "The Next, Next Generation tablet."
} ];

$scope.select = function(phone) {
$scope.editedPhone = phone;
};
}

Essayez, ça marche. Facile, n’est-ce pas ? Wahoo, quel super framework ! Enfin, la programmation d’IHM devient simple !

Voire…

Les petits malins, qui ont lu la doc d’Angular doivent se dire in petto : « Il est bien gentil, l’autre, avec son exemple, mais on peut faire encore mieux, plus concis. Puisqu’il est possible de placer des affectations dans une expression Angular, pourquoi ne pas remplacer, dans le template :

<li ng-repeat="phone in phones" ng-click="select(phone)">

par :

<li ng-repeat="phone in phones" ng-click="editedPhone = phone">

La définition du réflexe « select » devient inutile, n’est-ce pas ?

Essayez, ça ne marche pas. Pourquoi ? Parce que l’expression « editedPhone = phone » s’exécute dans le scope de la ligne, qui n’est pas disponible lors de l’édition du formulaire. Rappelez-vous : pour chaque ligne, un nouveau scope, très local, est créé. C’est dans ce scope là que la variable editedPhone est créée et renseignée. Dommage.

« Attendez ! » me direz-vous, « le réflexe « select » s’exécute aussi dans ce scope ! Donc l’affectation $scope.editedPhone = phone doit aussi se faire dans le scope courant, c’est-à-dire celui de la ligne ! Alors,  ça ne devrait pas marcher non plus ! » Eh bien si. Why ? Because les closures de Javascript.

Explication : Le $scope utilisé dans le réflexe est celui qui a été passé en paramètre au contrôleur, au moment où celui-ci est exécuté, c’est-à-dire à la construction de la page. Angular n’y est pour rien, c’est Javascript le coupable. Il s’agit donc du scope lié au controller, pas à celui de la ligne. « Certes, mais la fonction PhoneCtrlList s’étant executée depuis longtemps, la valeur de ce paramètre a disparu avec la fin de son exécution, non ? » Eh bien non : c’est ça la magie des closures. Les méthodes définies au sein de la fonction « PhoneListCtrl » disposent toujours du contexte d’exécution de cette fonction et ce pour toute leur existence ! Le $scope ne disparaîtra pas…  Subtil, n’est-ce pas ? Si vous n’avez pas tout compris aux closures, une lecture attentive de la documentation afférente et quelques exercices s’imposent. Sinon, Angular risque d’être plus fort que vous !

C’est tout à coup moins facile, n’est-ce-pas ?

Si vous voulez faire fonctionner l’exemple donné ci-dessus écrivez « $parent.editedPhone = phone ».

Finalement, cet article a comme un début d’intérêt, n’est-ce pas ?

Prochain épisode : comment connecter cette mini-appli à un véritable back-end en utilisant les services REST ?

Categories: Java EE Tags: ,
  1. Lara
    26/02/2015 à 11:56 | #1

    Très bon tutoriel. Dans le même cadre j’aimerais partager avec vous ce cours pour débutants et développeurs confirmé AngularJS http://goo.gl/dmHZyx Merci beaucoup.

  1. 31/01/2014 à 11:01 | #1
  2. 10/02/2014 à 14:47 | #2


neuf − 3 =