version 1.1.0, dernière mise à jour le 2 décembre 2015.
Les Single Page Applications sont des applications Web particulières où l'ensemble des données et leur gestion sont contenus dans une seule et même page. Cela a pour avantage de ne pas avoir à charger de nouvelles pages lors, par exemple, d'une validation de formulaire mais il faut aussi, dans ce cas, que la page soit en mesure de modifier son propre DOM en réponse à des actions de l'utilisateur. Des frameworks
JavaScript
le permettent, comme Ember, Backbone ou... Angular.
Un patron de conception, ou design pattern, est un ensemble de bonnes pratiques et de conventions de codage et d'organisation. Un de ces design patterns répandus est le patron «" Modèle-Vue-Contrôleur ». Il permet de concevoir en théorie séparément :
le modèle, c'est-à-dire les données qui seront utilisées;
la vue, à savoir la manière dont ces données sont restituées à l'utilisateur;
le contrôleur, qui gère les flux de données entre le modèle et la vue.
La bibliothèque Angular est structurée autour de quelques concepts-clefs :
le module, qui désigne l'application elle-même;
le modèle. Les données sont structurées en JSON.
le contrôleur
des « directives » et des filtres qui permettent de gérer les manières dont les données sont traitées avant leur restitution via le DOM
le DOM lui-même, qui correspond à la vue.
Nous allons voir par la suite comment ces structures s'articulent.
Avant toute chose, il faut télécharger la bibliothèque depuis le site officiel, puis la charger avec par exemple <script src="angular.min.js"></script>
. Ce cours est rédigé sur la base d'Angular 1.4.x
Côté JavaScript, on va définir un module, qui ne sera autre que l'application, ainsi que jeter les bases du contrôleur :
var monApp = angular.module("monApp", []) ;
monApp.controller("monControleur", ["$scope", function ($scope){
//Code du contrôleur...
}]) ;
Côté HTML, on liera l'application et le contrôleur respectivement avec les attributs data-ng-app
et data-ng-controller
(on peut aussi les écrire ng-app
et ng-controller
mais cette seconde syntaxe n'est pas valide en HTML5). Par exemple :
<div data-ng-app="monApp">
<div data-ng-controller="monControleur">
<!-- Là où seront restituées les données couvertes par le contrôleur -->
</div>
</div>
C'est par l'intermédiaire de l'objet $scope
, dont le nom est prédéfini en Angular, que sera défini le modèle. Il est tout à fait possible de déclarer plusieurs contrôleurs pour un même module :
var monApp = angular.module("monApp", []) ;
monApp.controller("controleurPrincipal", ["$scope", function ($scope){
//Code du premier contrôleur...
}]) ;
monApp.controller("controleurDonneesUtilisateur", ["$scope", function ($scope){
//Code du second contrôleur...
}]) ;
Il est très facile d'importer dans la vue (le DOM), les données issues du modèle. Par exemple...
<div data-ng-app="monApp">
<div data-ng-controller="monControleur">
{{ texte }}
</div>
</div>
var monApp = angular.module("monApp", []) ;
monApp.controller("controleurPrincipal", ["$scope", function ($scope){
$scope.texte = "Bonjour!" ;
}]) ;
$scope
fait référence à l'élément courant dans le DOM. Cet objet permet la communication entre le DOM et la source de données (statique comme c'est le cas ci-dessus, ou bien dynamique si elles proviennent du serveur). On peut donc y trouver des initialisations de variables comme ici, ou bien des fonctions permettant l'échange de données avec le serveur. Mais en aucun cas ne doivent donc s'y trouver de manipulations du DOM.
Bien évidemment, les données peuvent être plus complexes...
var monApp = angular.module("monApp", []) ;
monApp.controller("controleurPrincipal", ["$scope", function ($scope){
$scope.utilisateur={} ;
$scope.utilisateur={
"prenom" : "Douglas" ,
"nom" : "Alavanillecilvouplaix" ,
"dateDeNaissance" : 1995
} ;
}]) ;
Angular permet aussi de lier dynamiquement les données modifiées au niveau de la vue, au modèle, et ce sans passer par une étape intermédiaire. On pourra par exemple, sans écrire de contrôleur, directement écrire...
<div data-ng-app="monApp" data-ng-controller="monControleur">
<p>
<label for="quantite">Nombre</label>
<input type="number" id="quantite" data-ng-model="nombre">
</p>
<p>Nombre sélectionné : {{ nombre }}</p>
</div>
Ce qui donne...
Les directives sont potentiellement complexes, puisque ce sont elles qui permettent de gérer la manière dont les données seront exploitées et restituées à la vue.
Il existe quatre manières d'appeler une directive dans le DOM. Nous allons voir comment en créant une directive, nommée nouveau-parag
ajoutant un nouveau paragraphe :
en tant qu'élément : <nouveau-parag>Blabla</nouveau-parag>
en tant qu'attribut : <p data-nouveau-parag>Blabla</p>
en tant que classe : <p class="nouveau-parag">Blabla</p>
en tant que commentaire : <!-- directive: nouveau-parag -->
L'utilisation en tant qu'attribut est la plus cohérente avec la philosophie et la recommandation de HTML5. On peut d'ailleurs écrire, plus simplement, <p nouveau-parag>Blabla</p>
mais cela produit du code invalide.
La documentation de l'API d'Angular est complète. Nous n'aborderons ici qu'une poignée des directives prédéfinies, afin d'en montrer le principe. Tout d'abord, remarquons que si une directive s'écrit avec un tiret dans le DOM, son nom sera écrit avec des majuscules intercalaires dans le JavaScript :
ng-cloak
permet de masquer les composants Angular tant que l'ensemble du DOM n'a pas été créé. Cela permet, en cas de création massive de contenu via le framework, de ne pas avoir à l'affichage de disgracieuses accolades.
les événements HTML sont aussi doublonnés en Angular. On retrouve par exemple ng-click
, notée ngClick
en JavaScript. Par exemple...
<div data-ng-app="monApp" data-ng-controller="monControleur">
<p>
<button data-ng-click="nombre = nombre+1 " data-ng-init="2">Incrémentez-mol! !</button>
</p>
<p>Nombre : {{ nombre }}</p>
</div>
ng-copy
, ng-paste
et ng-cut
sont des directives qui peuvent être employées pour gérer la copie, le collage ou la coupe de texte.
<div data-ng-app="monApp" data-ng-controller="monControleur">
<div>
<label for="chptxt">Texte à saisir</label>
<input id="chptxt" data-ng-init="texte = ''" data-ng-copy="texte='le champ a été copié'" data-ng-paste="texte='le champ a été collé'" data-ng-cut="texte='le champ a été coupé'">
</div>
<p>État de la sélection : {{ texte }}</p>
</div>
ng-repeat
permet de faire une boucle. Par exemple
<div data-ng-app="monApp" data-ng-controller="monControleur" data-ng-init="courses = [
{'nom':'Café','quantite':'1 boîte'},
{'nom':'Thé','quantite':'2 boîtes'},
{'nom':'Chocolat','quantite':'1 boîte'},
{'nom':'Confiture de fraise','quantite':'1 pot'},
{'nom':'Confiture d\'abricot','quantite':'1 pot'},
{'nom':'Pain','quantite':'1 baguette'}
]">
<p>Il y a {{courses.length}} trucs à acheter pour le petit déjeuner. Ce sont :</p>
<ul>
<li data-ng-repeat="truc in courses">{{truc.nom}} ({{truc.quantite}})</li>
</ul>
</div>
ng-style
permet de gérer le style d'un élément. Par exemple...
<div data-ng-app="monApp" data-ng-controller="monControleur">
<p>Choisissez la couleur du texte :</p>
<label rouge="Rouge">
</label>
<input type="radio" id="rouge" name="couleur" data-ng-click="stylePerso = {'color': 'red'}">
<label bleu="Bleu">
</label>
<input type="radio" id="bleu" name="couleur" data-ng-click="stylePerso = {'color': 'blue'}">
<p data-ng-style="stylePerso">Texte dont la couleur va changer.</p>
</div>
Enfin, il est possible de définir une nouvelle directive. Reprenons notre exemple initial de directive nouveau-parag
. Nous allons définir la nouvelle directive avec la méthode directive
, en lui attribuant le nom nouveauParag
selon la même convention de transformation des tirets en majuscules. La définition d'une directive renvoie une directive :
monApp..directive('nouveauParag', function(){
return {
template : "<p>Ceci est un nouveau paragraphe.</p>"
}
}
Dans le HTML, on utilisera la directive ainsi...
<div data-ng-controller="monControleur" data-nouveau-parag>
</div>
La méthode directive
renvoie un objet directive
, constitué des propriétés suivantes :
template
permet de spécifier ce qui va remplacer le balisage qui doit être injecté. Ce balisage sest interprété par Angular, donc on peut y inclure des bindings et autres codes Angular ;
replace
permet d'indiquer si le balisage doit remplacer le balisage existant ou non (attention, cette option disparaîtra à partir de la version 2.0 d'Angular) ;
restrict
permet de spécifier comment la directive peut être utilisée : par attribut ('A'
), élément ('E'
), commentaire ('M'
) ou classe ('C'
). La valeur par défaut est 'AE'
;
templateURL
: quand le balisage est volumineux, on ne peut pas le faire tenir intégralement dans template
. templateURL
permet d'indiquer une URL où le trouver. Cela offre de plus l'avantage de permettre la mise en cache de ce template par le navigateur.
Les filtres, comme leur nom l'indique, permettent de préciser le résultat d'une directive, d'une recherche, ou de filtrer la saisie dans un champ de formulaire. Il existe un certain nombre de filtres prédéfinis, mais tout comme pour les directives, il est possible de définir ses propres filtres. La syntaxe générale est de la forme {{ expression | filtre : paramètres du filtre}}
Là encore, la documentation officielle d'Angular donne de nombreux exemples. Voici quelques filtres utiles, mais il y en a d'autres :
date
permet de mettre en forme une date. Par exemple, {{ date_expression | date: "dd/MM/yyyy"}}
permet d'afficher une date au format jour/mois/année.
limitTo
tronque un tableau à un nombre d'éléments donné. Si le nombre spécifié est négatif, la troncature du tableau se fait à partir de ses derniers éléments. Par exemple...
<div data-ng-controller="monControleur" data-ng-init="tableau=[5,2,3,8,0,1,29,-6,5,5,11,3]">
<label for="chplimite">Limite</label>
<input type="number" id="limite" data-ng-init="limite=10" data-ng-model="limite">
<p>{{limite}} éléments du tableau initial {{tableau}} de {{tableau.length}} éléments affichés : {{tableau | limitTo: limite}}</p>
</div>
orderBy
permet de spécifier un ordre de tri. On écrira orderBy:clefDeTri
ou orderBy:clefDeTri:true
selon que l'on veut un tri par ordre direct ou inversé.
<div data-ng-controller="monControleur" data-ng-init="liste=[{'nom':'Vaistairstalone', 'prenom':'Cécile'},{'nom':'Danlavoiturpourpassalir', 'prenom':'Amadeus'},{'nom':'Zeblouze','prenom':'Agathe'},{'nom':'Binuneptitsieste', 'prenom':'Geoffrey'}]">
<ul>
<li data-ng-repeat="personne in liste|orderBy:'prenom':true">{{personne.prenom}} {{personne.nom}}</li>
</ul>
</div>
lowercase
et uppercase
permettent de convertir en minuscules ou majuscules les chaînes de caractères, par exemple {{personne.nom|lowercase}}
Angular permet la création de nouveaux filtres avec la méthode filter
. Par exemple...
monApp.filter("permute", function(){
return function(entree) {
var sortie = '' ;
for (var i=0;i<entree.length;i++) {
switch(entree[i]) {
case 'a': sortie+='e';break ;
case 'e': sortie+='i';break ;
case 'i': sortie+='o';break ;
case 'o': sortie+='u';break ;
case 'u': sortie+='y';break ;
case 'y': sortie+='a';break ;
defaultsortie+=entree.charAt(i) ;
}
}
return sortie ;
}
}) ;
<div data-ng-controller="monControleur" data-ng-init="chaine='Texte n\'ayant pas de sens mais y a-t-il besoin d\'en avoir un?'">
<p>Sans filtre : {{chaine}}</p>
<p>Avec filtre : {{chaine | permute}}</p>
</div>
Les fonctions de scope
permettent, dans le contrôleur, de changer les données du modèle. Il s'agit bien ici de modifier les données, mais en aucun cas de modifier le DOM.
var monApp = angular.module("monApp", []) ;
monApp.controller("monControleur", ["$scope", function ($scope){
$scope.supprime = function (index)
{
$scope.liste.splice(index, 1) ;
}
}]) ;
<div data-ng-controller="monControleur" data-ng-init="liste=[{'nom':'Vaistairstalone', 'prenom':'Cécile'},{'nom':'Danlavoiturpourpassalir', 'prenom':'Amadeus'},{'nom':'Zeblouze','prenom':'Agathe'},{'nom':'Binuneptitsieste', 'prenom':'Geoffrey'}]">
<ul>
<li data-ng-repeat="personne in liste" data-ng-click="supprime($index);">{{personne.prenom}} {{personne.nom}}</li>
</ul>
</div>
Dans le code précédent, Angular se charge de reconnaître quel élément il convient de supprimer à l'aide de la variable $index
Cette création est mise à disposition par Gilles Chagnon sous un contrat Creative Commons.