Présentation

Nous allons (enfin) nous attaquer au vif du sujet : coder avec AngularJS ! :)

Pour ce faire, nous allons développer la mini application qui m'a servi d'illustration au chapitre précédent, la Todo List dont vous pouvez voir le rendu final sur cette Live Demo.

Capture d'écran de la Todo List
Capture d'écran de la Todo List

Les étapes

Pour écrire cette application, nous allons suivre le plan suivant :

  1. Création du module todoList
  2. Rédaction du template HTML
  3. Implémentation du contrôleur

Cette application étant particulièrement simple, nous n'allons pas séparer physiquement les différentes sections du Javascript. On laissera tout notre code dans un seul fichier todoList.js. Mais lorsque l'on développera une véritable application, on prendra bien soin d'organiser notre code. Nous en reparlerons le moment venu dans les parties suivantes.

Le module todoList

L'application

Afin de pouvoir utiliser un module sous AngularJS, il est nécessaire d'avoir une application. C'est elle qui ensuite appellera notre module lorsque nous le souhaiterons. Encore une fois, c'est le principe de modularité. Même si l'on pourrait penser que c'est se rajouter du travail pour rien, cette façon de procéder nous permettra par la suite de pouvoir réutiliser très facilement notre module dans un autre projet.

Pour créer une application, il faut la déclarer dans le template ET le Javascript, ce qui paraît logique. :) Dans notre cas, on l'appellera demoApp.

Dans le template

Comme on l'a vu dans le chapitre précédent, il faut déclarer l'application AngularJS via l'attribut ng-app. Il n'est pas obligatoire de le placer dans la balise <html> mais c'est plus pratique comme ça.
Bien sûr on lie tout de suite le CSS, AngularJS et notre code Javascript au HTML dans le <head>.
Télécharger angular.js depuis le CDN de Google

Pour aller plus vite, voici la feuille de style que j'ai utilisée : style.css. C'est une version ultra-light de Bootstrap 3 (uniquement les éléments de Typographie, les bouttons et les formulaires) avec quelques règles spécifiques pour l'occasion. Libre à vous de l'utiliser ou de personnaliser votre Todo List selon vos goûts.

<!DOCTYPE html>
<html lang="fr" ng-app="demoApp">
    <head>
        <meta charset="utf-8" />
        <title>Todo List</title>        
        <link rel="stylesheet" href="web/css/style.css">
        <script src="js/vendor/angular.js"></script>
        <script src="js/todoList.js"></script>
    </head>
    <body>
        <header>
            <h1>Todo List</h1>
        </header>
        <section>
            ...
        </section>
    </body>
</html>

On verra dans la partie 3 qu'il peut être intéressant d'inclure les fichiers Javascript à la fin bas du fichier HTML (juste avant la balise </body>) pour optimiser le chargement de la page lors d'une visite. Mais ceci peut avoir des conséquences indésirées si des éléments du template doivent être masqués au début. Bien entendu on verra comment éviter ce problème le moment venu, mais ce serait trop pour l'instant. ;)

Dans le Javascript

Pour déclarer l'application demoApp du côté Javascript, c'est très simple :

// js/todoList.js
'use strict';


/**
 * Déclaration de l'application demoApp
 */
var demoApp = angular.module('demoApp', [
    // Dépendances du "module"
    'todoList'
]);

Quelques explications s'imposent tout de même :

L'instruction
'use strict';
Sans trop rentrer dans les détails du mode strict en Javascript (disponible depuis ECMAScript 5), sachez qu'il permet d'éviter les variables globales qui sont fortement déconseillées si l'on n'a pas une raison très précise de s'en servir (et qu'on les maîtrise parfaitement). Toujours dans cette idée, je vous conseille de toujours utiliser le mot-clé var lorsque vous déclarer une variable.
Mozilla Developer Network : Le mode strict de ECMAScript 5
D'autre part, comme AngularJS utilise lui-même ce mode, le fait de ne pas l'utiliser pourrait déclencher des erreurs chez certains (vieux) navigateurs. Bref, ajoutez cette instruction avant toute autre dans TOUS vos fichiers Javascript utilisant AngularJS et vous n'aurez plus à y penser ! ;)
Un module ?!
Et oui... notre application n'est autre qu'un module au sens d'AngularJS, c'est-à-dire un élément (qui ne soit pas un contrôleur, un service ou une directive) qui a des dépendances et un nom. Comme d'habitude, on spécifie ces dépendances dans un tableau en deuxième argument.
Vous vous demandez sûrement pourquoi on s'embête à créer une application, qui n'est autre qu'un module finalement, si ce n'est que pour appeler notre véritable module todoList. En fait, vous allez voir (dès le chapitre suivant sur le routage) que notre application peut utiliser en fait d'autres modules. Et si vous souhaitez ensuite lui en ajouter encore d'autres (un service de chat, un mini-twitter, que sais-je ? :D ), il faudra bien les séparer puisque chacun aura ses propres dépendances. D'où cette distinction essentielle entre votre application et ses modules. De plus, si un jour vous souhaitez réutiliser votre module todoList, vous n'aurez qu'à le copier/coller sans prendre avec tous les autres modules qu'utilise l'application demoApp.

Le module

À présent, nous n'avons plus qu'à déclarer notre module todoList auquel on attachera le contrôleur :

/**
 * Déclaration du module todoList
 */
var todoList = angular.module('todoList',[]);

Et c'est tout ! Le module n'a pas de dépendance qui doive être résolue. On lui donne son nom et c'est tout bon. :)

Le template

Maintenant que nous avons tout préparé, nous n'avons plus qu'à faire les parties marrantes !

Spécifier le contrôleur

On commence bien sûr par dire à AngularJS quel contrôleur s'occupera de la section en question (via la directive ng-controller qu'on a vue au chapitre précédent) :

<section ng-controller="todoCtrl">

</section>

Ajouter un todo

Ensuite, on ajoute le champ texte qui permet d'ajouter un todo à notre liste :

<form id="todo-form" ng-submit="addTodo()">
    <input id="new-todo" placeholder="Que devez-vous faire ?" ng-model="newTodo" />
</form>

Ici on voit apparaître 2 directives dont on a pas parlé jusqu'à présent :

ng-model
Cette directive est extrêmement pratique et vous vous en servirez quasiment tout le temps ! Elle permet d'ajouter une variable dans le scope correspondant à l'élément concerné (ici, le scope du contrôleur todoCtrl). Vous vous rappelez du scope ? C'est un ensemble de variables au sein du modèle et qui permet à AngularJS de constamment lier ces variables entre la vue et le contrôleur. Revoir le chapitre précédent sur le scope
Ainsi, en appliquant cette directive à une balise input, select ou textarea (ou autre), vous pouvez connaître et contrôler la valeur de ces éléments. Documentation sur la directive ngModel (EN)
ng-submit
Elle permet de déclencher une fonction (variable du scope de l'élément) lorsque le formulaire est soumis (touche Entrée ou champ input de type submit). En l'occurrence, on appelle la fonction addTodo() qui est définie dans le contrôleur. Documentation sur la directive ngSubmit (EN)

Les identifiants des balises HTML de cette page servent UNIQUEMENT à appliquer le CSS (même si c'est une pratique à bannir en temps normal, on admettra que cette mini-application n'a pas à être optimisée ici). Ils n'interviennent en aucun cas dans le Javascript comme pourrait le penser un développeur habitué à jQuery par exemple. AngularJS n'utilise que les directives pour se repérer dans notre Todo List.

Les todos enregistrés

Puis on ajoute la partie où sont tous les todos déjà enregistrés. Grâce à la directive ng-repeat (vue au chapitre précédent elle aussi), on n'écrit qu'un template et non une vue : on donne à AngularJS la forme que doit prendre chaque entrée et celui-ci s'occupe de la reproduire pour chacune en remplaçant les variables par les valeurs correspondantes de chaque entrée.

<article ng-show="todos.length">
    <ul id="todo-list">
        <li ng-repeat="todo in todos" ng-class="{completed: todo.completed}">
            <div class="view">
                <input class="mark" type="checkbox" ng-model="todo.completed" />
                <span>{{todo.title}}</span>
                <span class="close" ng-click="removeTodo(todo)">x</span>
            </div>
        </li>
    </ul>
</article>

Détaillons un peu les directives nouvelles que l'on vient de rencontrer :

ng-class
Cette directive permet d'assigner une classe à n'importe quelle balise en fonction d'une condition. Ici, on dit que la balise <li> aura la classe quand todo.completed == true. Sinon, elle n'aura aucune classe. ng-class est très complète et vous pouvez assignez plusieurs classes en fonction de nombreuses conditions. Documentation sur la directive ngClass (EN)
ng-click
Relativement facile à comprendre, ng-click correspond au vieil attribut (invalide XHTML et HTML 5) onclick. Elle permet d'appeler une fonction (avec ou sans argument) suite au click sur un élément, tout simplement. Documentation sur la directive ngClick (EN)
ng-show
Celle-ci est un petit peu plus subtile. Elle dit à AngularJS de n'afficher l'élément concerné seulement si la condition donnée entre guillemets est vraie. Ici, l'élément <article> ne sera affiché que si todos.length != 0. Autrement dit, on n'affiche <article> que lorsqu'il y a au moins un todo d'enregistré. Documentation sur la directive ngShow (EN)

Enfin, on ajoute les boutons d'action en bas de la liste de todos, qui ne présentent aucune nouveauté :

<input id="mark-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)" />
<label class="btn btn-info" for="mark-all">Tout cocher</label>
<button class="btn btn-danger" ng-click="clearCompletedTodos()">Supprimer les tâches cochées</button>

Version finale

Voilà pour le template ! C'est finalement bien plus simple qu'il n'y paraît, car tout est logique et suit notre façon de concevoir l'application. Voici donc la version finale de notre template HTML :

<!DOCTYPE html>
<html lang="fr" ng-app="demoApp">
    <head>
        <meta charset="utf-8" />
        <title>Todo List</title>        
        <link rel="stylesheet" href="web/css/style.css">
        <script src="js/vendor/angular.js"></script>
        <script src="js/todoList.js"></script>
    </head>
    <body>
        <header>
            <h1>Todo List</h1>
        </header>
        <section ng-controller="todoCtrl">
            <form id="todo-form" ng-submit="addTodo()">
                <input id="new-todo" placeholder="Que devez-vous faire ?" ng-model="newTodo" />
            </form>
            <article ng-show="todos.length">
                <ul id="todo-list">
                    <li ng-repeat="todo in todos" ng-class="{completed: todo.completed}">
                        <div class="view">
                            <input class="mark" type="checkbox" ng-model="todo.completed" />
                            <span>{{todo.title}}</span>
                            <span class="close" ng-click="removeTodo(todo)">x</span>
                        </div>
                    </li>
                </ul>
                <input id="mark-all" type="checkbox" ng-model="allChecked" ng-click="markAll(allChecked)" />
                <label class="btn btn-info" for="mark-all">Tout cocher</label>
                <button class="btn btn-danger" ng-click="clearCompletedTodos()">Supprimer les tâches cochées</button>
            </article>
        </section>
    </body>
</html>

Je n'ai donné ici qu'un aperçu des directives natives d'AngularJS les plus courantes. Je donnerai un mode d'emploi détaillé de leur utilisation dans le chapitre dédié aux directives de la partie 2.

Le contrôleur

Spécifier le contrôleur

Avant tout, on déclare notre contrôleur, comme on l'a vu au chapitre précédent, avec son nom et ses dépendances. Ici, il ne dépend que du service $scope. Et par la même occasion, on initialise la liste des todos avec un tableau vide.

/**
 * Contrôleur de l'application "Todo List" décrite dans le chapitre "La logique d'AngularJS".
 */
todoList.controller('todoCtrl', ['$scope',
    function ($scope) {

        // Pour manipuler plus simplement les todos au sein du contrôleur
        // On initialise les todos avec un tableau vide : []
        var todos = $scope.todos = [];
    }
]);

J'ai profité de cette initialisation pour faire correspondre la variable todos à la variable $scope.todos. Il n'y rien de sorcier là dedans, c'est simplement pour ne pas avoir à réécrire $scope partout dans le contrôleur. Bien entendu, cette équivalence entre les deux variables n'est valable qu'à l'intérieur de cette fonction (c'est-à-dire ce contrôleur) et ce grâce au mot-clé var qui rend la variable locale.

Vous remarquerez à ce propos, qu'on se contente d'appeler le service $scope pour manipuler LE scope de notre contrôleur et uniquement celui-là. Cette petite magie est rendue possible grâce au système d'Injection de Dépendances (DI) dont nous avons parlé au chapitre précédent. Ainsi, à l'intérieur du contrôleur, chaque fois que vous appelez le service $scope, vous manipulez celui de ce contrôleur.

Attention à ne pas confondre le service $scope et UN scope comme par exemple celui du contrôleur ou encore celui de ng-repeat. Le service permet de manipuler un scope bien précis. La confusion est très facile puisque pour accéder à une variable / propriété du scope en question on utilise la syntaxe $scope.maVariable.

Ajouter un todo

Ensuite, on implémente la fonction qui permet d'ajouter un todo :

// Ajouter un todo
$scope.addTodo = function () {
    // .trim() permet de supprimer les espaces inutiles
    // en début et fin d'une chaîne de caractères
    var newTodo = $scope.newTodo.trim();
    if (!newTodo.length) {
        // éviter les todos vides
        return;
    }
    todos.push({
        // on ajoute le todo au tableau des todos
        title: newTodo,
        completed: false
    });
    // Réinitialisation de la variable newTodo
    $scope.newTodo = '';
};

Quelques remarques sur cette fonction :

Déclaration de
la fonction
À la ligne 30, on déclare bien la fonction comme une propriété du scope du contrôleur. C'est ce qui permet à AngularJS de la retrouver lorsque le template l'appelle.
Ajout du todo
À la ligne 38, on ajoute un todo au tableau des todos avec son titre et la propriété completed à false. Ce tableau étant agrandi d'une entrée, AngularJS mettra tout de suite à jour la vue selon le template : il ajoutera un élément <li> et si c'est le premier todo, il rendra visible la balise <article>. Sans qu'on ne fasse rien bien entendu :D
Two-Way Data
Binding
Et oui, encore lui :) . En fait c'est une façon pompeuse de vous faire remarquer qu'on peut effectivement lire ET modifier les variables du scope. La variable $scope.newTodo l'illustre parfaitement : on la lit d'abord (à la ligne 32) pour pouvoir créer le nouveau todo puis on la modifie (à la ligne 44) pour réinitialiser le champ input. Et tout prend effet instantanément grâce à AngularJS !

Les todos enregistrés

Enfin, on implémente les fonctions permettant de gérer les todos déjà enregistrés. Il n'y a aucune difficulté propre à AngularJS, on a déjà tout vu.

// Enlever un todo
$scope.removeTodo = function (todo) {
    todos.splice(todos.indexOf(todo), 1);
};

// Cocher / Décocher tous les todos
$scope.markAll = function (completed) {
    todos.forEach(function (todo) {
        todo.completed = !completed;
    });
};

// Enlever tous les todos cochés
$scope.clearCompletedTodos = function () {
    $scope.todos = todos = todos.filter(function (todo) {
        return !todo.completed;
    });
};

Quelques précisions peuvent être intéressantes tout de même :

markAll(completed)
La fonction prend un argument completed, l'état de la checkbox correspondant au bouton . Et l'on met la propriété completed de chaque todo à la même valeur que celle de la checkbox globale.
Cependant, on remarque qu'on donne la valeur opposée à celle passée en argument (grâce au !). En effet, au moment où la fonction est appelée, la checkbox n'a pas encore changé d'état.
clearCompletedTodos()
Cette fonction ne prend pas d'argument. On utilise la méthode .filter() pour ne récupérer que les todos qui ne sont pas cochés (todo.completed == false c'est-à-dire !todo.completed == true). On écrase alors l'ancien tableau pointé par todos et $scope.todos avec le résultat de .filter(). On est alors obligé de rappeler que ces 2 variables désignent le même tableau, sans quoi notre code ne fonctionnerait plus une fois la fonction exécutée. Je vous conseille d'ailleurs d'essayer de supprimer l'une des 2 variables à la ligne 16 pour voir ce que ça fait et comprendre la logique qui se cache derrière.
Documentation sur la méthode .filter()

Version finale

Finalement, après avoir préparé notre application et notre module, notre contrôleur n'est pas très dur à rédiger. Il suffit encore une fois de suivre la logique du framework. Voici donc la version finale du fichier todoList.js :

// js/todoList.js
'use strict';


/**
 * Déclaration de l'application demoApp
 */
var demoApp = angular.module('demoApp', [
    // Dépendances du "module"
    'todoList'
]);

/**
 * Déclaration du module todoList
 */
var todoList = angular.module('todoList',[]);


/**
 * Contrôleur de l'application "Todo List" décrite dans le chapitre "La logique d'AngularJS".
 */
todoList.controller('todoCtrl', ['$scope',
    function ($scope) {

        // Pour manipuler plus simplement les todos au sein du contrôleur
        // On initialise les todos avec un tableau vide : []
        var todos = $scope.todos = [];

        // Ajouter un todo
        $scope.addTodo = function () {
            // .trim() permet de supprimer les espaces inutiles
            // en début et fin d'une chaîne de caractères
            var newTodo = $scope.newTodo.trim();
            if (!newTodo.length) {
                // éviter les todos vides
                return;
            }
            todos.push({
                // on ajoute le todo au tableau des todos
                title: newTodo,
                completed: false
            });
            // Réinitialisation de la variable newTodo
            $scope.newTodo = '';
        };

        // Enlever un todo
        $scope.removeTodo = function (todo) {
            todos.splice(todos.indexOf(todo), 1);
        };

        // Cocher / Décocher tous les todos
        $scope.markAll = function (completed) {
            todos.forEach(function (todo) {
                todo.completed = !completed;
            });
        };

        // Enlever tous les todos cochés
        $scope.clearCompletedTodos = function () {
            $scope.todos = todos = todos.filter(function (todo) {
                return !todo.completed;
            });
        };
    }
]);

Conclusion

Comme vous l'avez sûrement remarqué, j'ai suivi exactement le même plan pour la rédaction du template et du contrôleur. Et comme vous pouvez vous en douter, ce n'est pas un hasard. :)

En effet, je tenais à vous montrer que construire un module sous AngularJS est relativement simple grâce à l'approche naturelle qu'il propose. Même si cela ne saute pas aux yeux lorsque l'on découvre ce framework, il permet pourtant d'aborder le front-end de façon limpide : on commence par spécifier le nom de notre module et ses contrôleurs (ici il n'y en qu'un seul) puis on implémente chacune de ses fonctionnalités, et c'est tout.

Que vous écriviez le contrôleur et le template de front ou l'un après l'autre comme je l'ai fait n'a strictement aucune importance. Chacun est plus à l'aise d'une façon ou d'une autre. Mais il faut bien comprendre qu'il y a une parfaite symétrie entre ces deux pendants du framework.

Avant d'attaquer la deuxième partie de ce tutoriel, il vaut mieux avoir vraiment bien compris et assimilé les notions vues jusqu'ici. Je vous invite donc à reprendre et personnaliser cette mini-application au maximum ! Même si elle est (volontairement) très simple, elle aborde beaucoup d'aspects essentiels d'AngularJS. Elle a tout de même servi de support à 2 chapitres (j'avais aussi un peu la flemme de chercher un autre exemple :p ). N'hésitez pas à enlever des instructions / directives ou en écrire de nouvelles pour voir ce que ça fait, c'est comme cela que vous avancerez le plus vite.

Un dernier petit conseil pour la route : même si vous ne connaissez pas encore tout sur AngularJS, n'hésitez jamais à regarder le code source des sites qui s'en servent. En essayant de comprendre leur logique, vous gagnerez en conception et vous penserez plus rapidement à telle ou telle solution lorsque vous rencontrerez un problème semblable sur votre chemin. Pour commencer, si vous avez observé un petit peu le code source de la Live Demo, vous aurez sans doute remarqué qu'il diffère du code qu'on a écrit ici. Rassurez-vous, je ne vous ai pas arnaqué, et les deux fonctionnent très bien. Simplement la demoApp que j'utilise en ligne n'est pas exactement la même que celle présentée ici. On verra dans la partie 2 comment elle est construite.

Pensez à laisser des commentaires si vous avez des questions / idées supplémentaires, etc. !

Vos commentaires

comments powered by Disqus