AngularJS repose en grande partie sur le patron de conception
(en anglais, Design Pattern) Modèle-Vue-Contrôleur. Ce design pattern — parce que ça sonne quand même mieux en anglais :) — se base, comme son nom l'indique, sur la subdivision conceptuelle de chaque page en un Modèle, une Vue et un Contrôleur.
J'ai précisé en partie parce que l'on verra dans les parties suivantes du tutoriel qu'AngularJS ajoute au Contrôleur d'autres outils comme les services et les directives. Mais le plus important de tous est bien le Contrôleur puisque c'est celui qui contient la logique de la page.
Si vous avez déjà développé des applications en PHP, avec ou sans framework (tel que Symfony2 ou Zend Framework 2), vous avez sûrement dû rencontré le modèle MVC. Même si le design pattern a le même nom, des différences notables sont à remarquer entre la version PHP, qui est plus classique, et celle proposée par AngularJS, surtout au niveau du modèle ! Lisez donc bien ce paragraphe en entier, vous y gagnerez en clarté.
Pour illustrer les explications quelque peu abstraite, je vais utiliser l'exemple basique de la Todo List (= liste de choses à faire
). Pour l'instant, je ne donne les sources que pour éclairer mes propos, ne cherchez pas à tout comprendre dès maintenant. Les chapitres suivants sont là pour çà ;) .
Vous pouvez d'ores-et-déjà voir le rendu final grâce à cette Live Demo.
La Vue
C'est ce que voit l'utilisateur final. Elle est générée à partir du template. Le template est le fichier HTML enrichi de certains attributs et balises propres à AngularJS que l'on découvrira au fur et à mesure.
Mème si par abus de langage La Vue
désigne parfois le fichier HTML, celui-ci n'est que le template. La vue n'est pas un fichier mais bien la projection de ce que voit le visiteur sur son navigateur. Et elle diffère du fichier HTML puisque des éléments apparaissent ou disparaissent grâce au Javascript, alors que le template lui ne change pas pendant toute la durée de vie de la page.
C'est la partie la plus facile à lire et à écrire. C'est d'ailleurs là que toute la puissance d'AngularJS saute aux yeux. Vous verrez que ce fichier sera incroyablement lisible même après de nombreux ajouts de fonctionnalités, contrairement par exemple aux fichiers de front-end n'utilisant que PHP.
Ci-dessous le template (à peine simplifié) de la mini-application Todo List
:
<!DOCTYPE html>
<html lang="fr" ng-app="demoApp">
<head>
<title>Todo List</title>
<link rel="stylesheet" href="web/css/style.css">
<script src="js/vendor/bower_components/angular/angular.js"></script>
<script src="js/todoList.js"></script>
</head>
<body>
<header>
<h1>Todo List</h1>
</header>
<section id="todo-list" 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" ng-cloak>
<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>
<i class="fa fa-times" ng-click="removeTodo(todo)"></i>
</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>
Moteur de template
AngularJS, intègre nativement un moteur de template (EN). Ceci permet d'utiliser des raccourcis syntaxiques pour afficher des variables ou même des entités logiques telles que les boucles et bien d'autres choses encore.
Il existe beaucoup de moteurs de template pour le web, en particulier pour PHP. Twig (celui intégré à Symfony2), par exemple, a une syntaxe très proche d'AngularJS — notamment avec les doubles accolades —, donc si vous l'avez déjà utilisé, vous ne serez pas dépaysé.
Sans chercher à tout apprendre dès maintenant sur la syntaxe d'AngularJS, on peut remarquer quelques points à partir de l'exemple ci-dessus.
- Le préfixe
ng-
- Marque de fabrique d'AngularJS, vous remarquez qu'on le trouve à toutes les balises où une action définie par le Javascript sera exécutée. Ce sont des directives AngularJS. On les découvrira peu-à-peu, mais si vous êtes curieux, vous avez la liste complète de celles qui existent et leur mode d'emploi dans la documentation (EN).
- La directive
ng-app
-
À la ligne 2, cette directive initialise l'application
demoApp
: AngularJS sait quel module il va devoir lire pour la page courante. Il génère par la même occasion le$rootScope
de la page. Plus d'informations sur le scope au paragrapheLe Modèle
- La directive
ng-controller
-
À la ligne 13, cette directive permet de dire à AngularJS que le contrôleur de cette section est
TodoCtrl
. Les variables qui seront à l'intérieur de cette balise seront accesibles et modifiables depuisTodoCtrl
. - Les
doubles accolades -
On remarque qu'à la ligne 22, on affiche le titre du
todo
avec une double accolade :{{todo.title}}
. Grâce au moteur de template d'AngularJS, cette séquence sera automatiquement — c'est-à-dire sans que le développeur n'ait à paramétrer quoi que ce soit ! — repérée et remplacée par la valeur de la variabletodo.title
. C'est magique ! - La boucle
ng-repeat
-
À la ligne 19, on remarque la directive
ng-repeat
. Elle permet de dire à AngularJS qu'il doit répéter l'élément en question (et tous ses éléments enfants) tant qu'il y a des entrées dans le tableautodos
... mais pas seulement ! À l'image d'une boucleforeach ($tableau as $entree)
de PHP, on dispose de la variable temporairetodo
à l'intérieur de la boucle, avec toutes ses propriétés. Ce qui est très pratique pour afficher son titre par exemple.
Bref, vous l'aurez compris, ce moteur de template va grandement vous faire gagner en clarté et rapidité. Pour ceux qui ne sont pas convaincus (même si je doute qu'il y en ait), voici la version PHP du template de la Todo List (bien entendu les actions Javascript ne sont pas implémentées, il faudrait un script réagissant en fonction des identifiants et classes) :
<!DOCTYPE html>
<html lang="fr">
<head>
<title>Todo List</title>
<link rel="stylesheet" href="web/css/style.css">
<script src="js/todoList.js"></script>
</head>
<body>
<header>
<h1>Todo List</h1>
</header>
<section id="todo-list">
<form id="todo-form">
<input id="new-todo" placeholder="Que devez-vous faire ?" />
</form>
<?php if(sizeof($todos) > 0) ?>
<article>
<ul id="todo-list">
<?php foreach ($todos as $todo) ?>
<li <?php if($todo["completed"]) echo 'class="completed"'; ?> >
<div class="view">
<input class="mark" type="checkbox" <?php if($todo["completed"]) echo 'checked'; ?> />
<span><?php echo $todo["title"]; ?></span>
<i class="fa fa-times"></i>
</div>
</li>
</ul>
<input id="mark-all" type="checkbox" <?php if($allChecked) echo 'checked'; ?> />
<label class="btn btn-info" for="mark-all">Tout cocher</label>
<button id="clearCompletedTodos" class="btn btn-danger">Supprimer les tâches cochées</button>
</article>
</section>
</body>
</html>
Le Contrôleur
C'est le cœur du code correspondant à la page. Comme je le dis plus haut, on met dans le contrôleur toute sa logique, c'est-à-dire la description des actions que la page doit être capable de réaliser... et rien d'autre ! :) On verra que tout ce qui ne correspond pas à ces actions aura sa place dans les services ou directives. En pratique, ce n'est qu'une grande fonction un peu particulière. Puisqu'un exemple est plus parlant que de longs discours, voici le contrôleur de la Todo List :
// 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;
});
};
}
]);
Comme pour le template, on peut remarquer un certain nombre de choses propres à AngularJS sans pour autant s'acharner à tout comprendre tout de suite :
- La déclaration
du contrôleur -
À la ligne 22, on déclare le contrôleur avec la fonction
controller()
. On verra plus tard ce que représentent les variablesdemoApp
ettodoList
, mais ce n'est pas très important pour l'instant.
On donne donc à ce contrôleur :-
Un nom (ici
TodoCtrl
) qui permettra de l'appeler quand on en aura besoin. -
La liste de ses dépendances. Celles-ci peuvent être des services ou des modules.
AngularJS met à notre disposition de nombreux outils qui ont chacun un rôle précis, les services. Chacun commence par un$
. Vous pouvez voir la liste complète des services natifs dans la documentation (EN). Un chapitre de la partie 2 est dédié aux services. Ici, nous n'avons besoin que du service$scope
. Plus d'informations sur le scope au paragrapheLe Modèle
.
Les modules peuvent être développés par n'importe qui et pour faire n'importe quoi :) . C'est ce qui permet la modularité d'Angularjs. - La fonction en elle-même de la ligne 22 à la ligne 65.
-
Un nom (ici
- Pas de manipulation
du DOM -
Si l'on est un habitué à jQuery, ou aux librairies qui imposent de toujours sélectionner un élément (par sa classe, son identifiant, etc...) pour ensuite effectuer des changements tant sur le fond (les nœuds textuels) que sur la forme (les balises HTML et leur style), on sera surpris de remarquer qu'ici l'application ajoute, supprime et modifie la page HTML sans aucun appel au DOM. Vous n'avez plus à vous embêter à calculer le parent, les différents enfants de tel ou tel nœud (parfois il y a des incompréhensions si l'on a fait un retour à la ligne dans le fichier HTML entre 2 balises par exemple...), AngularJS fait tout pour vous et sait directement de quel élément vous parlez !
Finie l'utilisation des sélecteurs$(".className").each(...);
. Vous allez enfin réellement coder du Javascript avec des variables dignes de ce nom ! ;)
Dependency Injection
On a vu dans la déclaration du contrôleur qu'on signalait ses dépendances. Ce système très puissant d'ajout de fonctionnalités à un contrôleur (ou d'autres composants comme on le verra par la suite) via des services ou modules est rendu possible grâce à la Dependency Injection (EN).
Un système d'Injection de Dépendances a pour rôle de créer un composant, de résoudre ses dépendances et de l'injecter lorsqu'il est nécessaire à un autre composant. Ainsi, chaque application, ou page de l'application dit au DI ce dont elle a besoin et l'on n'est pas obligé d'intégrer manuellement tous les morceaux de code que l'on va utiliser. Par exemple, pour la Todo List
, on a juste à dire que le contrôleur utilise le service $scope
et celui-ci sera utilisable à l'intérieur de TodoCtrl
, avec toutes ses variables et fonctions disponibles. Mais en plus, le DI nous assure qu'il n'y aura pas de conflits entre les différentes parties du code, même si elles utilisent les mêmes services... encore une fois, si c'est pas beau çà ! :)
Selon le logiciel auquel il est intégré, le DI impose une certaine syntaxe pour qu'il puisse savoir ce qu'est un composant, la façon de l'identifier, etc...
Voir la documentation sur la DI d'AngularJS (EN)
Nous verrons, lors du chapitre dédié aux contrôleurs, les différentes possibilités pour déclarer correctement un contrôleur. Mais sachez dès maintenant que celle que j'ai utilisée ici est la plus fiable et la plus complète.
Le Modèle
C'est la partie la plus abstraite du design pattern, mais elle n'est pas plus dure à comprendre que les autres.
Le modèle selon AngularJS, c'est l'ensemble des scopes de la page. Un scope, c'est un domaine dans lequel des variables (ou fonctions, on ne fera plus la différence maintenant) peuvent exister. C'est en quelque sorte une grande famille au sein de laquelle les variables peuvent intéragir sans risquer de modifier les autres familles de variables. Ce principe nous permet d'éviter les collusions et effets de bord. Bien sûr, ces familles ne sont pas complètement indépendantes les unes envers les autres : elles sont liées entre elles par des liens de parenté.
On peut se représenter ces familles de scopes sous la forme d'un arbre, comme un arbre généalogique par exemple. La racine de tous les scopes est le $rootScope
qui est instancié lorsqu'AngularJS voit la directive ng-app
(à la balise <html>
pour notre template). Vient ensuite le scope du contrôleur instancié par la directive ng-controller
, logique :) . Enfin, certaines directives créent leur propre scope, comme ici ng-repeat
. Une petite illustration de cette arborescence :
D'autre part, généralement, vous n'avez pas à déclarer ces scopes. L'initialisation de chaque scope de notre exemple a été faite par AngularJS lui-même, et il en sera ainsi dans la très grande majorité des cas d'utilisation. Voir la documentation d'AngularJS sur les scopes
Le service $scope
Étant donné que le contrôleur a pour but de manipuler les variables du modèle, il doit y avoir accès. Pour ce faire, il passe par le service $scope
. Celui-ci permet la constante et transparente synchronisation des variables entre le modèle et la vue. Ainsi, toute modification d'une valeur de variable de la vue est immédiatement répercutée dans le modèle et inversement. C'est ce service qui est donc au centre du Two-Way data Binding.
À partir de la version 1.2, AngularJS propose une nouvelle syntaxe utilisant ControllerAs
. Ceci permet d'éviter d'insérer obligatoirement le service $scope
à tous les contrôleurs en passant par this
. Cela allège la syntaxe du contrôleur mais il est important de savoir que this
et $scope
ne sont pas strictement équivalents.
Présentation vidéo de la syntaxe avec ControllerAs
(EN)
Différences entre this et $scope dans les contrôleurs AngularJS (EN)
Par habitude, je continue de déclarer le service $scope
dans les dépendances de tous mes contrôleurs, mais libre à vous de faire comme moi ou pas ;)
Vos commentaires