Technologies

AngularJS : Le tutoriel pour bien démarrer

Publié le : Auteur: Vincent Lemordant Laisser un commentaire
technologies

AngularJS, développé par Google depuis 2009, s’impose de plus en plus comme une référence dans le domaine des frameworks JavaScript. Une présentation détaillée est disponible sur ce blog : cliquez ici.

Avec AngularJS, la navigation, la récupération des données et leur traitement ont lieu côté client. Le serveur n’a plus qu’à valider et envoyer les données, ce qui fournit à l’utilisateur une navigation plus fluide. Cette déportation de la logique permet de développer aisément des Single Page Application, ces applications web à la mode, accessibles via une page web unique.

AngularJS apporte également de nombreux concepts importants :

  • La synchronisation automatique entre la Vue (ce que voit l’utilisateur) et le Modèle (les données manipulées par l’application).
  • La séparation des composants de l’application en blocs distincts : Contrôleurs, Services, Directives, Filtres, Vues…
  • L’injection de dépendances qui permet de lier et de réutiliser des composants externes ou internes à votre application.
  • La possibilité de tester facilement son code à l’aide de tests unitaires.

AngularJS et ses mécanismes novateurs ont su attirer une communauté active et toujours plus importante de développeurs, fournissant un support efficace au framework.
Ce tutoriel a pour but de vous fournir les bases de cet outil incontournable du Web d’aujourd’hui et de demain. Pour illustrer les différentes notions du framework, nous allons développer une application simple reprenant les mécanismes d’un site d’e-commerce proposant des fruits.

Création de l’application

Dans cette partie, nous allons voir comment créer notre application AngularJS.
Il faut tout d’abord inclure AngularJS dans le projet, soit depuis le site officiel, soit en utilisant un gestionnaire de packages.
Ensuite, nous faisons référence à ‘angular.js’ et nous déclarons notre application dans notre fichier HTML : index.html. On ajoute la balise « ng-app=’tutoApp' », qui permet de déclarer quelle application nous allons utiliser :

index.html

<!DOCTYPE html>
<html ng-app="tutoApp">
<head>
 <script src="Scripts/angular.js"></script>
 <script src="app.js"></script>
</head>
<body>
</body>
</html>

Il faut également déclarer l’application « tutoApp » dans un nouveau fichier JavaScript : app.js. Ici notre application n’est autre qu’un module, avec son nom et son tableau de dépendances. Pour l’instant elle dépend du module « fruitModule » :

app.js

var app = angular.module('tutoApp', ['fruitModule']);

Vous vous demandez peut être pourquoi nous déclarons une application (qui est un module) pour appeler un autre module « fruitModule »?
Au fur et à mesure de son évolution, notre application possédera plusieurs modules, avec chacun leur liste de dépendances ; pour cette raison, il faut dès le début distinguer notre application et ses modules. De cette manière, on pourra également réutiliser le module « fruitModule » dans une autre application, sans y ajouter toutes les dépendances de « tutoApp ».
On peut désormais déclarer « fruitModule » et lui attacher notre contrôleur :

app.js

var fruitModule = angular.module('fruitModule', []);
fruitModule.controller('fruitController', ['$scope', function ($scope) {
}]);

Le contrôleur est une fonction qui contient la logique de notre application. Il possède un nom et un tableau de dépendances. La syntaxe « [‘dependance‘, function (dependance) { }] » est verbeuse mais nécessaire si l’on souhaite travailler avec du code Angular minifié.

« Hello world! »

Pour l’instant notre contrôleur ne dépend que du service $scope (on reconnaît les services natifs à AngularJS par le préfixe ‘$’). Ce service va nous permettre de modifier le Scope (c’est-à-dire le contexte) du contrôleur. On va pouvoir y attacher des variables et les instancier. Pour plus d’informations sur les Scopes et le modèle selon AngularJS, je vous conseille cet article.

app.js

fruitModule.controller('fruitController', ['$scope', function ($scope) {
   $scope.fruit = "pomme";
}]);

Angular va ensuite s’occuper lui même de la synchronisation entre notre vue et notre modèle (i.e. les différents Scopes de l’application, dont le Scope du contrôleur). Il suffit de déclarer notre contrôleur dans le HTML comme suit:

index.html

<body ng-controller="fruitController">
{{fruit}}
</body>

Les doubles accolades vont être automatiquement remplacées au chargement par la valeur de la propriété ‘fruit’ du modèle et notre navigateur nous affichera « pomme ».

Cependant, ce mécanisme ne serait pas « bidirectionnel » si ça ne fonctionnait que dans un sens ! On peut également modifier le modèle directement depuis la vue à l’aide d’un input :

index.html

<input ng-model="fruit" />

Ecrire dans ce champ va modifier la valeur de la propriété ‘fruit’ du modèle, sans plus d’effort de notre part !

Objet complexe & filtre

On peut bien sûr utiliser des objets plus complexes au sein de notre application :

app.js

$scope.fruit = {
        Nom: "pomme",
        Couleur: "rouge",
        Peremption: new Date()
};

L’accès aux différentes propriétés de la variable ‘$scope.fruit’ est intuitif :

index.html

La {{fruit.Nom}} de couleur {{fruit.Couleur}} périme le {{fruit.Peremption}}

Cependant le format de la date retournée par le navigateur (« 2015-09-10T15:38:34.728Z ») n’est pas très élégant. C’est justement l’occasion d’appliquer un filtre pour lui donner le format souhaité. AngularJS possédant nativement plusieurs filtres (lowercase, uppercase, OrderBy, date,…), il serait dommage de s’en priver.
{{fruit.Peremption | date}} va nous formater la date de la manière suivante : « Sep 10, 2015 ».
Si l’on souhaite un formatage comprenant les heures et les minutes on peut écrire {{fruit.Peremption | date : « dd/MM à HH:mm »}}.

Et si l’on souhaite répéter ce formatage partout où on affiche une date, on doit le réécrire à chaque fois ? Heureusement non ! AngularJS pense à nous et nous donne la capacité de créer nos propres filtres. On peut écrire un nouveau filtre en partant de rien ou utiliser un filtre existant et le modifier. Nous privilégierons la seconde méthode ici, en réutilisant le filtre date :

app.js

fruitModule.filter("dateFilter", ['$filter', function ($filter) {
    return function (input) {
        date = $filter('date')(new Date(input), 'dd/MM à HH:mm');
        return date;
    }
}])

Il suffit d’appeler le nouveau filtre de la manière suivante {{fruit.Peremption | dateFilter}}. Le résultat est le même qu’avant mais le code HTML est plus lisible et on peut répéter le filtre où l’on veut dans le code.

Liste d’objets & tri

Nous allons désormais travailler avec une liste d’objets :

app.js

$scope.fruits = 
        [{
            Nom: "pomme",
            Couleur: "rouge",
            Peremption: new Date()
        },
        {
            Nom: "poire",
            Couleur: "verte",
            Peremption: new Date()
        },
        {
            Nom: "prune",
            Couleur: "violette",
            Peremption: new Date()
        }];

AngularJS possède une directive native très utile pour afficher les éléments d’une liste et leurs différentes propriétés : la directive ng-repeat. De la même manière qu’un forEach, elle permet d’accéder à tous les éléments d’un tableau. On peut ensuite faire passer l’élément à travers un template HTML pour récupérer ses propriétés.

index.html

<ul>
<li ng-repeat="fruit in fruits">la {{fruit.Nom}} de couleur {{fruit.Couleur}} périme le {{fruit.Peremption | dateFilter}}</li>
</ul>

AngularJS va répéter le template pour chaque élément du tableau ‘fruits’ et on obtiendra le résultat suivant :

  • La pomme de couleur rouge périme le 10/09 à 18:23
  • La poire de couleur verte périme le 10/09 à 18:23
  • La prune de couleur violette périme le 10/09 à 18:23

Cette directive est un gain de temps important pour le développeur et elle permet de laisser le code HTML simple et clair. On va également pouvoir filtrer ou trier cette liste, en écrivant ng-repeat= »fruit in fruits | OrderBy : ‘Nom' » par exemple.

Créer ses propres directives

Et si on veut répéter cette liste de fruit à différents endroits dans notre application ? Le mieux est de créer des composants maintenables et réutilisables : les directives. C’est une des forces de AngularJS, en plus de rendre le HTML dynamique il nous donne la capacité d’étendre le vocabulaire HTML ! Pour créer notre directive (dans le fichier JavaScript), on va procéder ainsi :

app.js

fruitModule.directive("fruitDirective", function () {
    return {
        template: "La {{fruit.Nom}} de couleur {{fruit.Couleur}} périme le 
        {{fruit.Peremption | dateFilter}}"
    };
})

Et pour inclure cette directive dans le HTML ? C’est très simple, on utilise des tags, des attributs, des classes ou des commentaires. En pratique, il est conseillé de les inclure à l’aide d’attributs :

<ul>
<li ng-repeat="fruit in Fruits | orderBy : 'Nom'" fruit-directive></li>
</ul>

Ou de tags :

<ul>
<li ng-repeat="fruit in Fruits | orderBy : 'Nom'" ><fruit-directive></fruit-directive></li>
</ul>

Ici notre nouvelle directive est excessivement simple et elle ne fait rien d’autre qu’afficher des informations sur le fruit. Cependant, vous verrez qu’on peut ajouter de nombreuses options lors de la création d’une directive, afin de lui assigner un comportement particulier.

Fonction & 2ème contrôleur

Nous avons désormais une liste de fruits, mais, comme toute application marchande qui se respecte, il faut pouvoir les ajouter dans un panier. Nous allons donc ajouter une méthode dans ‘fruitController’, afin de traduire cette fonctionnalité :

app.js

$scope.panier = [];
$scope.ajouterFruitAuPanier = function (fruit) {
        $scope.panier.push(fruit.Nom);
    };

Il faut également ajouter un bouton pour que l’utilisateur choisisse quel fruit il ajoute. L’appel à la fonction ‘$scope.ajouterFruitAuPanier’ se fera grâce à l’attribut ‘ng-click’, natif à AngularJS (c’est l’équivalent du ‘on-click’ de JavaScript) :

index.html

<ul>
 <li ng-repeat="fruit in Fruits | orderBy : 'Nom'">
   <fruit-directive></fruit-directive>
    <button ng-click="ajouterFruitAuPanier(fruit)">Ajouter le fruit au panier</button>
 </li>
</ul>
Mon panier contient : {{panier}}

C’est bien mais en réalité une application possède plusieurs contrôleurs. Pour rendre la notre plus « réelle » nous allons donc ajouter un contrôleur qui sera chargé de la gestion du panier (il pourrait calculer le prix du panier selon les fruits qu’il contient par exemple). On ajoute simplement le code suivant au fichier Javascript :

app.js

fruitModule.controller('panierController', ['$scope', function ($scope) {
    $scope.panier = [];
}])

On peut supprimer la ligne ‘$scope.panier = [];’ du contrôleur ‘fruitController’ car le panier est désormais instancié dans ‘panierController’.

Notre code HTML ressemble maintenant à ça :

index.html

<body>
<div ng-controller="fruitController">
<ul>
 <li ng-repeat="fruit in Fruits | orderBy : 'Nom'">
    <fruit-directive></fruit-directive>
    <button ng-click="ajouterFruitAuPanier(fruit)">Ajouter le fruit au panier</button>
 </li>
</ul>
</div>
<div ng-controller="panierController">
   Mon panier contient : {{panier}}
</div>
</body>

Et là on a un problème : quand on clique sur « Ajouter le fruit au panier », ‘fruitController’ essaye de remplir le panier de ‘panierController’. Or, nos deux contrôleurs ont chacun leur Scope (leur contexte) et ils ne peuvent pas interagir directement entre eux ! « $scope.panier » n’existe pas dans ‘fruitController’.
Heureusement, AngularJS nous permet de résoudre ce problème à l’aide des Services.

Les services

Nous en avons déjà utilisé un : « $scope ». Un service est un composant AngularJS qui permet d’organiser et de partager du code à travers notre application. C’est donc le candidat idéal pour partager le contenu de notre panier entre nos deux contrôleurs.

app.js

fruitModule.service("fruitService", [function () {
    this.panier = [];

    this.ajouterFruitAuPanier = function (fruit) {
        this.panier.push(fruit.Nom);
    }
}]);

Notre service contient le panier à partager et une fonction pour ajouter un fruit au panier. Avant de pouvoir l’utiliser dans nos contrôleurs, on va devoir leur donner accès à ce service et pour ça on utilise l’injection de dépendances :

app.js

fruitModule.controller('fruitController', ['$scope', 'fruitService', function ($scope, fruitService) {
}]);

fruitModule.controller('panierController', ['$scope', 'fruitService', function ($scope, fruitService) {
}])

On a presque fini, il nous reste juste à appeler ‘fruitService’ depuis nos contrôleurs. On modifie ‘fruitController’ de la manière suivante :

$scope.ajouterFruitAuPanier = function (fruit) {
        fruitService.ajouterFruitAuPanier(fruit);
    };

Et ‘panierController’ récupère le panier depuis le service :

$scope.panier = fruitService.panier;

C’est bon, vos contrôleurs peuvent tous deux accéder au panier !

La notion de routage

Puisque AngularJS déporte la logique côté client, l’utilisateur doit pouvoir naviguer entre les vues (et non pas les pages, car on est dans le cadre d’une Single Page Application) en restant indépendant du serveur. Le système de routage utilise donc des ancres (#) dans ces URL. Les ancres sont des ressources URL interprétées par le navigateur mais qui ne sont pas envoyées dans les requêtes. Le changement d’ancre nous permet de changer de vue sans effectuer de requête auprès du serveur et ainsi la navigation est plus fluide. Pour plus d’informations sur le routage, je vous conseille ce tutoriel.

Pour mettre en place le routage, il faut d’abord récupérer le module ‘ngRoute’ :

index.html

<head>
<script src="Scripts/angular.js"></script>
<script src="https://code.angularjs.org/1.4.5/angular-route.js"></script>
<script src="app.js"></script>
<title></title>
</head>

Puis l’inclure dans notre application à l’aide de l’injection de dépendances :

app.js

var app = angular.module('tutoApp', ['fruitModule', 'routageModule']);
var routageModule = angular.module('routageModule ', 'ngRoute']);

On découpe notre fichier index.html en trois différents fichiers.
index.html :

<!DOCTYPE html>
<html ng-app="tutoApp">
<head>
  <script src="Scripts/angular.js"></script>
   <script src="https://code.angularjs.org/1.4.5/angular-route.js"></script>
   <script src="app.js"></script>
   <title></title>
</head>
<body>
   <a href="#/fruit">Voir les fruits</a>
   <a href="#/panier">Voir le panier</a>

   <div ng-view></div>
</body>
</html>

index.html est notre « layout », c’est à dire un template HTML que l’on retrouvera sur toutes les vues de notre application. Nous y ajoutons des liens pour naviguer dans l’application.
La directive « ng-view » indique à AngularJS où inclure les autres templates HTML. Dans notre cas, ces templates sont : fruit.html et panier.html.
fruit.html:

<ul>
 <li ng-repeat="fruit in Fruits | orderBy : 'Nom'">
    <fruit-directive></fruit-directive>
    <button ng-click="ajouterFruitAuPanier(fruit)">Ajouter le fruit au panier</button>
 </li>
</ul>

panier.html:

 Mon panier contient : {{panier}}

Vous remarquerez que cette fois nous ne précisons pas quels contrôleurs nous allons utiliser. Nous le ferons dans notre table de routage.

D’ailleurs voici comment on remplit la table de routage : à chaque URL on fait correspondre un template HTML à charger (la vue) et le contrôleur utilisé dans cette vue. On peut également définir un URL pour rediriger les utilisateurs en cas d’URL erronés (otherwise).

app.js

var routageModule = angular.module('routageModule', ['ngRoute']);
routageModule .config(['$routeProvider', function ($routeProvider) {
    $routeProvider
    .when('/fruit', {
        templateUrl: "fruit.html",
        controller: "fruitController"
    })
    .when('/panier', {
        templateUrl: "panier.html",
        controller: "panierController"
    })
    .otherwise({
        redirectTo: '/fruit'
    })
}]);

Et voilà, désormais vous pouvez naviguer de vue en vue dans votre application. Au lancement, votre navigateur vous redirigera vers « /index.html#/fruit », le cas par défaut de la table de routage (otherwise).

Le serveur

AngularJS déplace la logique côté client mais il peut quand même communiquer avec une base de données, pour récupérer des données ou en poster par exemple. Dans notre cas le serveur est IIS et la base de données sera un simple fichier JSON contenant les différents fruits disponibles sur le site :

fruits.json

[
    { "Id": 1, "Nom": "Pomme", "Couleur": "rouge", "Peremption": "2015-08-25T07:00:00.000Z" },
    { "Id": 2, "Nom": "Poire", "Couleur": "verte", "Peremption": "2015-08-25T07:00:00.000Z" },
    { "Id": 3, "Nom": "Prune", "Couleur": "violette", "Peremption": "2015-08-25T07:00:00.000Z" } 
]

Afin de récupérer des données aux formats JSON, veuillez vérifier que le serveur connait le type MIME des fichiers JSON, sinon, si vous utilisez IIS, ajoutez dans votre ficher Web.config le code suivant :

Web.config

<system.webServer>
   <staticContent>
      <mimeMap fileExtension=".json" mimeType="application/json" />
   </staticContent>
</system.webServer>

La récupération des données se fait à l’aide du service $http. Ce service permet de formuler des requêtes AJAX vers des serveurs. Il contient notamment les méthodes get, post, put, delete, respectivement utilisées pour récupérer, créer, éditer et supprimer des données dans la base de données.
Dans notre cas, la façon la plus simple de récupérer les données est la suivante :

app.js

fruitModule.controller('fruitController', ['$scope', 'fruitService', '$http', function ($scope, fruitService, $http) {

    $http.get('fruits.json').then(
       function (response) { $scope.Fruits = response.data; }
       function (error) { alert(error); }
    );

    $scope.ajouterFruitAuPanier = function (fruit) {
        fruitService.ajouterFruitAuPanier(fruit);
    };
}]);

En réalité « $http.get() » ne retourne pas directement les données mais renvoie une promesse. Une promesse est un objet JavaScript correspondant au résultat différé d’une opération asynchrone.
Je m’explique : le fait d’envoyer une requête au serveur est une opération qui va prendre un certain temps. Si cette opération était synchrone, elle bloquerait l’exécution de l’application jusqu’à ce qu’elle soit terminée (car le JavaScript n’est pas un langage multi-thread), ce qui n’est pas souhaitable. C’est pourquoi ici l’opération est asynchrone : l’opération renvoie immédiatement une promesse qui sera résolue ultérieurement.
Les promesses possèdent une méthode then(), prenant deux fonctions en paramètre : la 1ère exécutée en cas de succès et la 2nd en cas d’échec. Le paramètre de ces fonctions possèdent 4 propriétés : data, status, headers et config. Vous l’aurez deviné, dans notre cas c’est la propriété data qui nous intéresse.
Il suffit de faire « $scope.Fruits = response.data; » pour attacher ces données à la propriété Fruits du Scope du contrôleur. Vous trouverez de plus amples informations sur les promesses ici (Sachez que les méthodes success() et error() sont à éviter).

Une manière plus élégante et préconisée par AngularJS est d’effectuer l’appel au serveur depuis un service et d’inclure ensuite ce service dans le contrôleur, ce qui permet une meilleure séparation des responsabilités. Voici comment procéder :

app.js

fruitModule.service("serveurService", ['$http', function ($http) {
    this.getFruits = function () {
        return $http.get('fruits.json')
    };
}]);

fruitModule.controller('fruitController', ['$scope', 'fruitService', 'serveurService', function ($scope, fruitService, serveurService) {
    var promiseGet = serveurService.getFruits();

    promiseGet.then(
        function (response) {
            $scope.Fruits = response.data;
        },
        function (error) { alert(error); }
        );

    $scope.ajouterFruitAuPanier = function (fruit) {
        fruitService.ajouterFruitAuPanier(fruit);
    };
}]);

Conclusion

Nous n’avons fait qu’effleurer le framework et nous voyons déjà à quel point il est puissant. Avec AngularJS vous pourrez proposer aux utilisateurs une expérience de qualité tout en ayant un code clair et lisible. Les services, les directives ou encore les filtres vous permettront de réutiliser vos composants où vous le souhaitez dans votre application. Pour finir, la communauté importante et active du framework vous aidera à surmonter bien des difficultés.

En espérant vous avoir convaincu 🙂

Applications réalisées avec AngularJS
Projets en cours ou terminés utilisant AngularJS

Sources :
Site officiel d’AngularJS : https://angularjs.org/
Blog francophone sur AngularJS : http://www.frangular.com
Tutoriel français sur AngularJS : http://www.tutoriel-angularjs.fr/