AngularJS
Mikael Couzic
Charles Jacquin
Table des matières
- AngularJS in a nutshell
- Les composants
- Angular core
- Environnement de développement
- Principales directives
- NgMaterial
- UiRouter
- Validations de formulaires
- Les services
- Gestion du modèle
- Modules complémentaires
- Tests unitaires et fonctionnels
"AngularJS is a new, powerful, client-side technology that provides a way of accomplishing really powerful things in a way that embraces and extends HTML, CSS and JavaScript, while shoring up some of its glaring deficiencies. It is what HTML would have been, had it been built for dynamic content."
Miško Hevery
AngularJS in a Nutshell
Adoption croissante
Popularité explosive
Recrutements en hausse
Pourtant, pas toujours facile...
Premier projet AngularJS
"Le web 2.0 est un méli-mélo d’outils et de sites qui encouragent la collaboration et la participation : Flickr, YouTube, MySpace, Wikipedia et l’ensemble de la blogosphère en sont quelques exemples. Ces sites ne sont pas des lieux d’achat, mais des zones de partage de données électroniques et de contact entre internautes."
Web 2.0
Rich
- Interactif
- Dynamique
Internet
- S’exécute dans un navigateur web
- Avec ou sans plugin
Application
… et pas Site
RIA
Le Web "Riche"
Dans les années 90, les RIA c’est… des applets Java !
Dans les années 2000, on voit apparaître…
- Adobe Flash
- GWT (2006)
- Silverlight (2007)
- ExtJS (2007)
- JavaFX (2008)
- Les frameworks MVC Client
- Backbone.js (2010)
- Ember.js (2011)
- AngularJS (2012)
Et demain ?
Orienté Composant
Orienté Modèle
jQuery est orienté DOM
L'essentiel des applications développées en jQuery reposent sur la manipulation intensive du DOM sur un mode impératif. Pour modifier la donnée, on modifie le DOM. Pour obtenir la donnée, on parcourt le DOM.
AngularJS est orienté Modèle
Dans une application AngularJS, la majorité du code manipule le modèle, simple représentation objet de la donnée. La vue est ensuite définie en fonction de l'état du modèle, sur un mode déclaratif.
Data-binding
Data Binding made right !
Data Binding en action !
<div ng-app>
<h2>Hello, {{user.firstName}} {{user.lastName}}</h2>
<div class="panel">
<label>First Name</label>
<input type="text" ng-model="user.firstName">
<label>Last Name</label>
<input type="text" ng-model="user.lastName">
</div>
<div class="panel">
<label>First Name (again)</label>
<input type="text" ng-model="user.firstName">
<label>Last Name (again)</label>
<input type="text" ng-model="user.lastName">
</div>
</div>
ng-app et ng-model sont des directives.
{{ }} permet de binder notre modèle à notre vue.
Équivalent jQuery
<div>
<input type="text">
Hello, <span id="name"></span>
</div>
$(document).ready(function() {
var $input = $('input');
var $span = $('#name');
$input.keyup(function (event) {
$span.text(event.target.value);
});
});
L'objet Angular
- angular.copy
- angular.isDefined
- angular.equals
- angular.extend
- angular.merge
- angular.forEach
- angular.fromJson
- angular.toJson
- angular.is...
- angular.lowercase
- angular.uppercase
- angular.noop
- angular.version
Fusionner deux objets
var src = {
foo: 'foo',
nested: { // ne sera pas copié
thing: 4
}
};
var dest = {
bar: 'bar'
};
angular.extend(dest, src);
var newObject = angular.extend({}, src, dest);
angular.extend permet de fusionner plusieurs objet.
angular.merge fusionne plusieurs objets récursivement. (deep merge)
var src = {
foo: 'foo',
nested: { // sera copié
thing: 4
}
};
var dst = {
bar: 'bar'
};
angular.merge(dest,src);
var newObject = angular.merge({},src,dist);
Les Composants
- Modules
- Templates (vue)
- Controllers
- Scopes
- Directives (extensions de HTML)
- Filters
- Services
Les Modules
Les modules angular permettent de découper l'application.
Une application angular est un module.
Relations de dépendances entres les modules.
Limiter au maximum le couplage en réduisant les dépendances entre modules.
Créer un module
angular.module('nomDuModule', ['dépendance1', 'dépendance2'])
Le deuxième argument de la fonction est un tableau contenant les noms de tous les modules dont dépend directement celui que l'on crée.
Attention !
angular.module('nomDuModule')
Ce code ne crée pas un nouveau module, mais récupère l'instance d'un module existant. Il est conseillé de ne pas utiliser cette version pour éviter les confusions.
Bonnes pratiques
Un module par fichier !
On a donc un appel à la fonction angular.module() par fichier
Cela favorise un meilleur découplage de l'application
[Update] ou pas...
Dans certains environnements de développement, un module par dossier, voire par application, peut être une solution adaptée
Dépendances
Au lancement de l'application, toutes les dépendances transitives sont chargées dans un injecteur unique, qui aura la responsabilité de fournir l'injection de dépendance à toute l'application
Attention !
Si deux modules ou deux services ont le même nom dans la même application, l'un des deux sera écrasé de manière transparente, la source du problème étant difficile à trouver
-> penser à une stratégie de namespacing pertinente !
Templates (vue)
- Fragments de HTML réutilisables
- Compilés par AngularJS
- Les vues générées sont dynamiques (Data Binding)
- Certaines conventions imposent l'extension .tpl.html
Template (suite)
<section>
some content
</section>
<script type="text/ng-template" id="myDialog.tpl.html">
</script>
On peut également déclarer un template imbriqué dans un autre template .
Controllers
<section ng-app="app">
<h1 ng-controller="HelloWorldController">
{{ message }}
</h1>
<div ng-controller="AnotherController as ctrl">
{{ ctrl.content }}
</div>
</section>
ng-controller est une directive.
$scope est un "service"
function HelloWorldController($scope) {
$scope.message = "Hello, World !";
};
function AnotherController() {
this.content = 'Another Content';
};
angular.module('app', [])
.controller('HelloWorldController', HelloWorldController)
.controller('AnotherController', AnotherController);
Il est injecté dans le controlleur par le framework
Scopes
Héritage de Scopes
Héritage de Scopes
- Chaque controller possède un scope
- Les scopes AngularJS sont organisés en arborescence hiérarchique
- Cette structure reflète l'organisation du DOM
- A la racine, on trouve le $rootScope
- Chaque scope hérite des propriétés de son parent
- selon les règles de l'héritage par prototype
- tout les scopes héritent du $rootScope (par transitivité)
Exemple d'héritage
Avec $scope
Avec "controller as" (sans $scope)
Propagation d'événement
Exercice
- Créez un module "app" et utilisez-le dans la vue
- Créez un controlleur "MyController" au sein de votre module et utilisez-le dans la vue avec la syntaxe "controller as"
- Affichez le texte "Hello World" dans votre vue par le biais de votre controller
Instructions :
Directives
- Composants graphiques
- Création ou décoration
- Utilisées comme des extensions à HTML
- Encapsule les manipulations du DOM
- Création difficile à maîtriser
Principales Directives AngularJS
- ng-app
- ng-controller
- ng-model
- ng-bind
- ng-click
- ng-if
- ng-show
- ng-hide
- ng-class
- ng-href
- ng-src
- ng-repeat
<section>
<section ng-controller="MyFirstCtrl">
...
</section>
<section ng-controller="MySecondCtrl as secondCtrl">
...
</section>
</section>
ng-controller
Permet de lier un controlleur à un element du DOM
Amené à disparaître avec Angular 2
ng-model & ng-bind
ng-model permet de "data binder" un champ de formulaire :
- input
- select
- autres directives comme tinymce ou ace
- ...
<input type="text" ng-model="user.name"/>
<p>{{ user.name }}</p>
<p ng-bind="user.name"></p>
ng-bind permet d'afficher le contenu de notre model, c'est l’équivalent des moustaches {{ }}.
ng-click
<section ng-app="app">
<div ng-controller="MyController as ctrl">
<button ng-click="ctrl.logIn()">login</button>
</div>
</section>
function MyController() {
this.logIn = function() {
alert("Login Successful");
}
}
angular.module('app', [])
.controller('MyController', MyController);
On retrouve les principaux événements natif :
- ng-mouseover
- ng-dblclick
- ng-keypress
- ...
Toutes ces directives fonctionnent sur le modèle de ng-click.
De la même manière :
ng-If
<div ng-app="app" ng-controller="LoginController as ctrl">
<div ng-if="!ctrl.isLogged">
<p>Please Login</p>
<button ng-click="ctrl.isLogged = true">Login</button>
</div>
<div ng-if="ctrl.isLogged">
<p>Welcome !</p>
<button ng-click="ctrl.isLogged = false">Logout</button>
</div>
</div>
function LoginController() {
this.isLogged = false;
}
angular.module('app', [])
.controller('LoginController', LoginController);
ng-show & ng-hide
ng-if, ng-show et ng-hide offrent un comportement similaire.
Contrairement à ng-if, ng-show et ng-hide se contentent de masquer ou rendre visible l'element avec l'attribut CSS :
display: none;
display: block;
Exercice
- Faire en sorte que le paragraphe apparaisse lorsque le pointeur de la souris survole le titre.
Instructions :
ng-class
<section ng-app="app">
<div ng-controller="MyController as vm">
<button ng-click="vm.myValue = !vm.myValue">Toggle class</button>
<p ng-class="{ 'my-class' : vm.myValue }">lorem ipsum blablabla ....</p>
</div>
</section>
function MyController() {
this.myValue = false;
}
angular.module('app', [])
.controller('MyController', MyController);
.my-class {
color: white;
text-shadow: 2px 2px 4px black;
}
Exercice
- Appliquer ou enlever (toggle) la classe CSS "shadow" au paragraphe lorsque l'utilisateur double-clique sur le titre.
Instructions :
ng-href & ng-src
Pour éviter d'éventuelles erreurs 404, nous pouvons utiliser ces deux directives qui attendront que l'URL soit bien évaluée avant d'afficher ou d'activer l'élément du DOM.
<section ng-app="app">
<div ng-controller="MyController as ctrl">
<img ng-src="{{ ctrl.user.picture }}" alt="{{ ctrl.user.name }}"/>
<a ng-href="{{ ctrl.user.url }}">{{ ctrl.user.name }}</a>
</div>
<section>
function MyController(){
this.user = {
name: 'foo',
picture: 'http://cloud/mypicture.png',
url: 'http://blog.com'
}
};
angular.module('app', [])
.controller('MyController', 'MyController');
ng-repeat
Chaque itération de la boucle reçoit son propre scope, qui possède la propriété $index
Cette directive permet d'itérer sur une collection :
- Array
- Object
Au lieu du traditionnel for dans le code JavaScript, nous n'aurons ici qu'un simple attribut HTML
Exercice
- Utiliser la directive ng-repeat pour afficher toutes les informations stockées dans le tableau du controller
- Afficher l'image en premier puis un titre suivis d'un lien
- Ecrire une methode "removeFramework()" dans le controller, qui prend un number en parametre et supprime le framework correspondant dans le tableau frameworks
- Vous pouvez utiliser les classes Bootstrap pour votre liste
Instructions :
Les filtres sont des composants fournis par Angular qui permettent de modifier la manière dont le modèle est affiché par la vue.
Les Filtres
Combinée avec ng-repeat, ils nous permettent d'améliorer le comportement de notre collection :
- limiter le nombre d'élément affichés
- filtrer a l'aide d'un input (par exemple)
Exercice
- Reprendre la fin de l'exercice précedent.
- Rajouter une input et appliquer un filtre sur la liste en filtrant par name.
Instructions :
Création de directives simples
Composants réutilsables ou simples décorateurs
<hello-world></hello-world>
Cette dernière sera ensuite utilisable comme une extension au langage html.
Nous allons créer une directive helloWorld qui affichera simplement "Hello, World !" à l'endroit où elle est utilisée
Directive Hello World
Hello World - Template
function helloWorldDirective(){
return {
restrict: 'E',
template: '<div class="panel">Hello, World !</div>'
}
}
angular.module('app', [])
.directive('helloWorld',helloWorldDirective);
<section ng-app="app">
<hello-world>
</section>
Hello World - TemplateUrl
<section ng-app="app">
<hello-world>
</section>
<div class="panel">
Hello, World !
</div>
function helloWorldDirective(){
return {
restrict: 'E',
templateUrl: '/hello/hello.tpl.html'
}
}
angular.module('app', [])
.directive('helloWorld',helloWorldDirective);
/hello/hello.tpl.html
Restrict
A la création d'une directive, on peut spécifier de quelle manière elle peut être utilisée dans les templates HTML
On spécifie pour cela une ou plusieurs valeurs dans l'option restrict
:
- E (element/balise)
- A (attribut)
- C (classe CSS)
- M (commentaire HTML)
Par défaut, la directive sera de type A (attribut)
Fonction link
La fonction link est exécutée une fois pour chaque instance de la directive, lors de son initialisation. On l'utilise surtout dans le cas où l'on veut manipuler le DOM de manière programmatique
Le nom des arguments n'a d'importance que pour suivre la convention. C'est l'ordre des arguments qui assure leur injection correcte.
Elle reçoit quatre arguments :
- scope : scope de la directive
- element : élément HTML correspondant à la balise où la directive a été appliquée
- attrs : attributs HTML appliqués à l'élément
- ctrls : le ou les controllers dont cette directive dépend
AngularJS & jQuery
AngularJS et jQuery fonctionnent bien ensemble.
Si jQuery est présent l'objet "element" fourni par la méthode link sera un objet jQuery.
Dans le cas contraire, angular utilise jqLite, une version allégée de jQuery.
jqLite
Text
angular.element('.foo');
// equivalent Jquery
$('.foo');
L'objet renvoyé par angular partage un certain nombre de méthodes avec un objet jQuery traditionnel, comme attr ou bind.
link function - Element
angular.module('app', [])
.directive('myShadow',myShadowDirective);
function myShadowDirective(){
return {
restrict: 'A',
link: function(scope,element,attrs){
element.toggleClass('shadowed');
}
}
}
On peut donc manipuler notre element html comme on le ferait avec jQuery
element est un objet jqLite
<p my-shadow>
Pellentesque habitant morbi tristique
</p>
Binder des évènements
angular.module('app', [])
.directive('myShadow',myShadowDirective);
function myShadowDirective(){
return {
restrict: 'A',
link: function(scope,element,attrs){
element.toggleClass('shadowed');
element.bind('click',function(){
element.toggleClass('shadowed');
})
}
}
}
Notre element est desormais clickable
Modules complémentaires
- ngRoute
- ngAnimate
- ngAria
- ngResource
- ngCookies
- ngTouch
- ngSanitize
- ngMock
Quelques librairies utiles...
- UI-Bootstrap
- UI-Router
- UI-Calendar
- angular-material
- ionic
- angular-file-upload
- restangular
- angular-formly
- angular-translate
- angular-charts
- angular-treeRepeat
- ngSocketIO
Services
- Une seule instance d’un service par application AngularJS
- Equivalent à un Singleton
- Le système d'injection de dépendance gère l'instanciation
- On peut créer un service pour partager des données entre plusieurs controllers
Description
Services fournis par AngularJS
- Les services fournis par le framework sont préfixés avec « $ » :
- $scope
- $rootScope
- $window
- $timeout
- $log
- $q
- $http
Nous allons pouvoir injecter ces différents services dans nos controllers, nos directives ou même nos propres services
Traitement asynchrone
function doAsyncCall(data,callback){
data++;
callback(data);
}
var data = 1;
doAsyncCall(data,function(firstData){
doAsyncCall(firstdata,function(secondData){
doAsyncCall(secondData,function(thirdData){
doAsyncCall(thirdData,function(fourthData){
doAsyncCall(fourthData,function(fifthData){
doAsyncCall(fifthData,function(finalData){
//do something with finalData
});
});
});
});
});
});
DOOM
Pyramid of doom
$q
function MyController($q, $timeout) {
function randomPromise() {
return $q(function(resolve, reject) {
if (Math.random() < 0.5) { // true une fois sur deux
resolve('successful');
} else {
reject(new Error('not successful :('));
}
});
}
this.asyncOperation = function() {
randomPromise()
.then(function(data) {
console.log(data)
})
.catch(function(err) {
console.error(err);
});
};
};
angular.module('app', [])
.controller('MyController', MyController);
function doAsyncCall(data) {
return $q(function(resolve,reject) {
if(typeof data !== 'number'){
reject(new Error('error !!!'));
} else {
resolve(data++);
}
});
}
var data = 1;
doAsyncCall(data)
.then(doAsyncCall)
.then(doAsyncCall)
.then(function(newData) {
return doAsyncCall(newData);
})
.then(function(finalData) {
console.log(finalData) //affiche 4
})
.catch(function(err) {
console.error(err);
});
Chainer des promises
function doAsyncCall(data) {
return $q(function(resolve,reject) {
if(typeof data !== 'number') {
reject(new Error('error !!!'));
} else {
resolve(data++);
}
});
}
var data = 1;
var anotherData = 2;
$q.all([
doAsyncCall(data),
doAsyncCall(anotherData),
anotherPromise
]).then(function(data) {
//data est un tableaux avec 3 entrées
}).catch(function(err){
});
Traitements parallèles
$http
Le service $http va nous permettre d'effectuer des appels AJAX vers un serveur de manière extrêmement simple.
$http({
method : 'GET',
url : '/users',
headers : {
Authorization: 'Bearer khksjhfkjshfjkhshfkjshfuhuhgi'
}
}).success(function(data){
//do something with response's data
}).error(function(err){
//something wrong happens
})
Toutes les méthodes de ce service retournent des promises, augmentées des méthodes success et error.
$http.get('/login',{
params : {
user : 'foo'
}
}).success(function(data){
//do something with the server's response
}).error(function(err){
//something wrong happens
})
//send an http request to /login?user=foo
GET
Permet d'envoyer une requête GET vers une url.
$http.post('/users',{
pseudo : 'foo',
password : 'bar'
}).success(function(data){
//do something with server response
}).error(function(err){
//something wrong happens
})
POST & PUT
Permet d'envoyer une requête POST ou PUT vers une url.
Le deuxième paramètre est l'objet envoyé dans le corps de la requête.
$http.delete('/users/01').success(function(data){
//do something with data
}).error(function(err){
//something wrong happens
})
$http.head('/users/01').success(function(data){
//do something with data
}).error(function(err){
//something wrong happens
})
Les autres methodes
Delete
Head
Providers
Pour chaque service disponible, AngularJS fournit l'accès à son provider
Le nom du provider est composé du nom du service, suivi par 'Provider' ($http -> $httpProvider)
Les providers ne sont injectables qu'en phase de configuration
Cycle de vie d'un module
Config
Phase permettant de configurer les services en paramétrant leurs providers. Seuls les constantes et providers sont injectables.
Run
Phase d'initialisation de l'application donnant accès aux instances des services. Les providers ne sont plus injectables.
angular.module('myModule', [])
.config(function(someServiceProvider) { ... });
angular.module('myModule', [])
.run(function(someService) { ... });
Intercepteur http
angular.module('app',[])
.config(function ($httpProvider) {
$httpProvider.interceptors.push(function ($window, $log) {
return {
request: function addTokenToHeader(httpConfig) {
var token = $window.localStorage.getItem('token');
if (token) {
httpConfig.headers.Authorization = 'Bearer ' + token;
}
return httpConfig;
},
requestError: function (rejection) {
$log.error(rejection); // error object
// Return the promise rejection.
return $q.reject(rejection);
},
response: function (response) {
$log.info(response); // response object
// Return the response or promise.
return response || $q.when(response);
},
responseError: function (rejection) {
$log.error(rejection); // error object
return $q.reject(rejection);
}
};
});
});
Création de services
Pour créer nos services nous disposons de 5 méthodes :
- constant
- value
- service
- factory
- provider
La méthode service
module.service - Exemple
function MyService() {
this.name = 'My Service Name';
this.purpose = 'For demo only';
this.sayHello = function(name) {
return 'Hello, ' + name;
};
}
angular.module('app', [])
.service('MyService', MyService);
AngularJS instancie le service avec le mot clef new
La méthode factory
Une factory n'est rien d'autre qu'une fonction qui retourne un objet.
C'est l'objet retourné par la factory qui sera injecté dans les autres services et controllers.
Factory - Exemple simple
function MyServiceFactory() {
return {
name: 'My Service Name',
purpose: 'For demo only',
sayHello: function(name) {
return 'Hello, ' + name;
}
}
}
angular.module('app', [])
.factory('MyService', MyServiceFactory);
Angular instancie le service en appelant la méthode factory
Exercice
- Encapsuler le code asynchrone dans une factory.
- Utiliser les méthodes de cette factory dans le controlleur.
Instructions :
Les méthodes constant & value
module.constant
Permet de définir des constantes. L'intérêt de cette méthode est qu'elle permet de définir des valeurs qui seront injectables dans la phase de configuration des autres services.
module.value
Peu utilisée, car les services déclarés ainsi ne peuvent pas dépendre d'autres services.
Les Providers
Les providers sont des services que nous allons pouvoir paramétrer avant que l'application n'ait complètement demarrée (durant la phase de config).
La méthode config de notre module angular nous permet de paramétrer nos provider,
angular.module('myApp',[])
.config(function(myServiceProvider){
//do some init job with myServiceProvider
})
angular.module('myModule',[])
.provider('myService',function(){
var monParam;
this.initMethod = function(value){
monParam = value;
}
this.$get = function(){
return {
methodOne : function(){ console.log(monParam) },
methodTwo : function(){}
}
}
})
Anatomie d'un provider
MonParam fait ici office d'attribut privé, en général un objet de configuration.
initMethod est la méthode qui sera appelable dans le bloc config.
$get est tout simplement le constructeur.
Provider - Exemple
(function(){
'use strict';
function helloWorld() {
var self = this;
var name = 'Default';
self.$get = function() {
var name = this.name;
return {
sayHello: function() {
return "Hello, " + name + "!"
}
}
};
self.setName = function(name) {
this.name = name;
};
}
function MyController(helloWorld) {
var self = this;
self.hellos = helloWorld.sayHello();
}
angular.module('app', [])
.provider('helloWorld', helloWorld)
.controller('MyController',MyController)
//hey, we can configure a provider!
.config(function(helloWorldProvider){
helloWorldProvider.setName('World');
})
})()
Configurable et injectable en phase de configuration
- name est une variable privée.
- $get est le constructeur
- setName est une methode utilisable en phase de configuration
Environnement de Développement
Environnement de développement.
Npm est le gestionnaire de paquet pour NodeJS, permet notament d'installer gulp et bower.
$ npm search nom-du-module
$ npm install nom-du-module
$ sudo npm install nom-du-module -g
$ npm install
$ npm update
Outil de gestion de dépendance front-end, sorte de npm ou de maven orienté js front-end.
$ sudo npm install -g bower
Dans un terminal
$ bower search nom-du-module
$ bower install nom-du-module
$ bower install
$ bower update
Outil d'automatisation de taches.
- Lance les tests
- lint le code (qualimétrie)
- minification du code (build)
- génération de documentation
- compilation Scss, Less, TypeScript ...
$ sudo npm install gulp -g
Dans un terminal
$ sudo npm install -g live-server
$ cd app
$ live-server
serveur de développement node.js
recharge automatiquement le navigateur
Démarer le serveur
$ sudo npm install -g json-server
$ json-server data.json
permet de simuler une api REST
fonctionne grâce à un fichier .json (ici data.json)
Pour démarer le serveur
Récuperation du code source
Dans WebStorm cliquer sur :
VCS > Checkout from version control > GIT
Préparation
$ npm i
Installation des dépendances
Démarer le serveur
$ cd app
$ live-server
Ainsi que le web-service
$ json-server data.json
Préparation (gulp)
Les taches gulp sont à définir dans le fichier gulpfile.js
$ gulp help
Afin de voir la liste des taches gulp disponible se positionner dans le repertoire du projet et utiliser :
TDD - Principe
Pour lancer les tests unitaires en continu, ouvrir un terminal :
$ gulp tdd
UI-Router
bower install ui-router --save
angular.module('app',[
'ui.router'
])
<script src=""></script>
Single Page Application
Dans une Single Page Application (SPA), la navigation est gérée coté client par un module spécialisé dans la manipulation de l'historique du navigateur.
Jusqu'à sa version 1.2, AngularJS était fourni avec le service de routage ngRoute. Ce composant étant limité et peu populaire, il n'est plus chargé par défaut.
Entre temps, le projet UI Router a vu le jour. Il est aujourd'hui considéré comme le router standard de fait pour AngularJS.
... jusqu'à la version 1.5, où un nouveau router fera son apparition.
UI Router - Composant
Services
$state
$stateParams
Directives
ui-sref
ui-view
Fonctionnement
La mise en place du routage se fait dans le bloc config du module, à l'aide du provider du service $state : $stateProvider.
-
On ne gère pas des url mais des états
-
Un état est toujours associé à une url et au moins un template
-
Facultativement, on peut lier un état à un controlleur
-
Possibilité de définir plusieurs vues par état
-
Possibilité d'avoir des vues imbriquées
ui-view
ui-view est la directive qui indique à UI Router où injecter le template associé à un etat
<body>
<ui-view>
<ui-view>
</body>
<body>
<div ui-view>
<div>
</body>
ou
Configuration de route
angular.module('myModule', ['ui.router'])
.config(function ($stateProvider) {
$stateProvider.state('home', {
url: '/',
templateUrl: 'home.tpl.html',
controller: 'HomeCtrl'
});
});
Plusieurs vues dans un state
<body>
<section ui-view="main"></section>
<section ui-view="adBanner"></section>
</body>
$stateProvider.state('users.create', {
url: '/list',
views: {
main: {
template: '...'
},
adBanner: {
template: '...'
}
}
});
Routes imbriquées
.config(function ($stateProvider) {
$stateProvider.state('users', {
url: '/users',
views: {
main: {
templateUrl: 'users.tpl.html'
}
}
});
$stateProvider.state('users.list', {
url: '/list',
views: {
users: {
templateUrl: 'users-list.tpl.html',
controller: 'UserListCtrl as users'
}
}
});
<body>
<div ui-view="main">
</div>
</body>
<div ui-view="users">
</div>
users.tpl.html
index.tpl.html
Routes abstraites
.config(function ($stateProvider) {
// /admin n'est pas accessible car abstract
$stateProvider.state('admin', {
abstract: true,
url: '/admin',
views: {
main: {
templateUrl: 'admin.tpl.html'
}
}
});
// /admin/blog
$stateProvider.state('admin.blog', {
url: '/blog',
views: {
admin: {
templateUrl: 'admin.blog.tpl.html',
controller: 'AdminBlogController as blog'
}
}
});
Ici l'utilisateur ne pourra pas se rendre à l'url /admin.
//index.html
<body>
<header></header>
<main ui-view="main"></main>
<footer></footer>
</body>
// admin.tpl.html
<header></header>
<section ui-view="admin"></section>
<footer></footer>
ui-sref
ui-sref se substitue à l'attribut href.
Quelques differences cependant :
Il peut s'appliquer a plusieurs éléments HTML (pas seulement les a) ;
On lui passe un état et non une url ;
<ul>
<li ui-sref="state1">state1</li>
<li ui-sref="state2">state2</li>
</ul>
ui-sref-active
ui-sref-active permet d'appliquer une classe css à un element ui-sref selon l'état courant.
cette directive s'utilise conjointement avec ui-sref
<ul>
<li ui-sref-active="active">
<a ui-sref="state1">
</li>
<li ui-sref-active="active">
<a ui-sref="state2">
</li>
</ul>
$state
Le provider du service $state permet de definir les differentes routes de notre application.
On peut également utiliser directement le service $state pour naviguer programmatiquement
$state.go('users.list');
Equivalent de window.location.href
resolve
$stateProvider.state('user', {
url: '/user/:id',
controller : 'UserController as ctrl',
templateUrl : 'user.tpl.html',
resolve: {
user: function($http, $stateParams) {
return $http.get('/users/' + $stateParams.id);
}
}
});
.controller('UserController',function(user){
this.user = user;
});
Gestion des erreurs
Ui-Router est trés mauvais pour gerer les erreurs.
Si une erreur se produit pendant le resolve, personne n'en saura rien.
Pour remédier à ça :
angular.module('app', ['ui.router'])
.config(function($stateProvider){
})
.run(function ($rootScope, $log) {
$rootScope
.$on('$stateChangeError', function (event, toState, toParams, fromState, fromParams, error) {
$log.error(error);
})
});
Interceptor
L'attribut onEnter permet de spécifier une fonction qui sera appelée avant la transition.
Il est possible d'injecter nimporte quel service dans cette fonction.
Pour remédier à ça :
$stateProvider.state('restricted', {
url: '/restricted',
onEnter: function(authService){
if(!authService.logged){
return false;
}
},
controller : 'RestrictedController as ctrl',
templateUrl : 'restricted.tpl.html'
});
UI Router Showcase
TP - Etape 1
Consignes :
Corrigé :
$ git checkout step1-userstate-solution
TP - Etape 2
Consignes :
Corrigé :
$ git checkout step2-moviestate-solution
TP - Etape 3
Consignes :
Corrigé :
$ git checkout step3-moviedetail-solution
ng-material
TP - Etape 4
Consignes :
Corrigé :
$ git checkout step4-dialog-solution
Validation de formulaire
Introduction
Pas de JavaScript (ou presque)
S'appuie sur les validations de formulaire HTML5
Fonctionne grâce à l'attribut name
L'attribut name
L'attribut name placé sur les balises form, input, etc... permet de facilement déterminer si un champ est valide ou non.
<form name="myForm">
<input type="text" name="username" ng-model="user.name">
</form>
Angular crée dans le scope un object myForm.
Pour accéder aux objets de validations des différents champs, il faut passer par cet objet myForm.
myForm.username nous renvoie l'objet de validation du champ username.
Le formulaire ainsi que ses champs possèdent les attributs suivants :
-
$valid : true si le formulaire est valide
-
$invalid : true si le formulaire est invalide
-
$pristine : true si le formulaire est vierge
-
$dirty : true si le formulaire à été utilisé
-
$touched : true si l'utilisateur à déja pris le focus sur l'input
-
$submit: true si l'utilisateur a essayer de submit le formulaire
-
$error : un objet contenant une référence vers chaque erreur de validation
Angular ajoute au formulaire et aux champs les classes CSS associées :
-
ng-valid
-
ng-valid-required
-
ng-invalid
-
ng-pristine
-
ng-dirty
-
ng-touched
Validation under the hood
<div ng-app="app" ng-controller="FormController as ctrl">
<form name="ctrl.userForm" ng-submit="ctrl.submit()" novalidate>
<input placeholder="username" required="true" type="text" name="username" ng-model="ctrl.user.name" />
<ul>
<li>valid : {{ ctrl.userForm.username.$valid }}</li>
<li>pristine : {{ ctrl.userForm.username.$pristine }}</li>
<li>error : {{ ctrl.userForm.username.$error }}</li>
</ul>
<input placeholder="email" type="email" name="email" ng-model="ctrl.user.email" />
<ul>
<li>valid : {{ ctrl.userForm.email.$valid }}</li>
<li>pristine : {{ ctrl.userForm.email.$pristine }}</li>
<li>error : {{ ctrl.userForm.email.$error }}</li>
</ul>
<button ng-disabled="!ctrl.userForm.$valid">Submit</button>
</form>
</div>
function FormController() {
this.submit = function() {
if (this.userForm.$valid)
alert('Form Submitted !!!\n' + angular.toJson(this.user));
else
alert('Invalid Form');
};
}
angular.module('app', [])
.controller('FormController', FormController);
Les directives de validation
AngularJS fournit tout un lot de directives qui permettent d'affiner la validation.
ng-required ;
ng-disabled ;
ng-maxlength ;
ng-minlength ;
ng-pattern ;
Certaines de ces directives comme ng-pattern ou ng-required reprennent des attributs HTML5.
Validation in action
<div ng-app="myApp" ng-controller="UserCtrl as user">
<form name="userForm" ng-submit="user.register()" novalidate>
<div>
<input type="text" ng-pattern="/^[a-zA-Z][a-zA-Z0-9-_\.]{3,10}$/"
ng-model="user.username" name="username" placeholder="Username"
required="" ng-minlength="3" ng-maxlength="10" />
<span ng-show="userForm.username.$error.maxlength">username must be 10 character maximum</span>
<span ng-show="userForm.username.$error.minlength">username must be 3 character minimum</span>
<span ng-show="userForm.username.$error.pattern">username must be only letter and number</span>
<span ng-show="userForm.username.$error.required && !userForm.username.$pristine">username is required</span>
</div>
<div>
<input type="email" ng-model="user.email" name="email" placeholder="Email" required />
<span ng-show="userForm.email.$error.email">enter a valid email</span>
<span ng-show="userForm.email.$error.required && !userForm.email.$pristine">email is required</span>
</div>
<div>
<input type="url" ng-model="user.url" name="url" placeholder="Url" required />
<span ng-show="userForm.url.$error.url">enter a valid url</span>
</div>
<div>
<button type="reset" ng-disabled="userForm.$pristine">Reset</button>
<button type="submit" ng-disabled="!userForm.$valid">Register</button>
</div>
</form>
</div>
ng-messages
Depuis la version 1.3 du framework, il existe un nouveau module qui simplifie encore la manoeuvre : ngMessage
Le module est composé de deux directives :
ng-messages
ng-message
bower install angular-messages --save
angular.module('app',[
'ngMessages'
])
<script src=""></script>
ng-messages in action
<div ng-app>
<form name="userForm" novalidate>
<div>
<input type="text" pattern="[a-zA-Z0-9]+" ng-model="user.username" name="username"
placeholder="Username" required="true" />
<div ng-messages="userForm.username.$error" ng-if="userForm.username.$touched || userForm.$submitted">
<div ng-message="required">
username is required
</div>
<div ng-message="pattern">
username must contain only letters or number
</div>
</div>
</div>
<div>
<input type="email" ng-model="user.email" name="email" placeholder="Email" required="true" />
<div ng-messages="userForm.email.$error" ng-if="userForm.email.$touched || userForm.$submitted">
<div ng-message="required">
email is required
</div>
<div ng-message="email">
enter a valid email
</div>
</div>
</div>
<div>
<button type="reset" ng-disabled="userForm.$touched">Reset</button>
<button type="submit">Register</button>
</div>
</form>
</div>
TP - Etape 5
Consignes :
Corrigé :
$ git checkout step5-form-solution
Architecture
"Business Model as a Service"
En résumé...
Le modèle ( état, méthodes ) doit toujours se trouver dans un service afin de pouvoir être distribué facilement aux différentes parties de l'application.
Factory - Exemple avancé
function PersonFactory() {
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Person.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}
Person.findById = function(id) {
// Send http request...
return new Person('John', 'Snow');
}
return Person;
}
angular.module('app', [])
.factory('Person', PersonFactory);
Cette Factory retourne une fonction constructeur, l'équivalent JavaScript d'une classe.
C'est légal car en JavaScript une fonction est un objet.
TP - Etape 6
Consignes :
Corrigé :
$ git checkout step6-createcomment-solution
Factory - Gestion du cache
function PersonFactory() {
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
Person.prototype.getFullName = function() {
return this.firstName + ' ' + this.lastName;
}
Person.findById = function(id) {
// Send http request...
return new Person('John', 'Snow');
}
return Person;
}
angular.module('app', [])
.factory('Person', PersonFactory);
Cette Factory retourne une fonction constructeur, l'équivalent JavaScript d'une classe.
C'est légal car en JavaScript une fonction est un objet.
Créer des directives complexes
Il est possible d'associer un controlleur à une directive.
Il est recommandé d'utiliser ici aussi la syntaxe "controller as"
Associer un controlleur
angular.module('app',[])
.directive('demo',demoDirective)
function demoDirective() {
return {
restrict: 'EA',
controllerAs: 'cardCtrl',
controller: function() {
//scope and services manipulation
},
link: function(scope, element, attrs) {
//Dom manipulation
}
}
}
La plupart des directives que l'on crée nécessitent d'avoir accès au modèle pour fonctionner. Pour ce faire, la bonne pratique est de passer les données à la directive via des attributs HTML.
Pour illustrer ce mécanisme, nous allons créer une directive hello qui prendra un nom en argument, et qui affichera "Hello, " + le nom
<hello name="Bob"></hello>
Attributs
Scope Isolé
Le fait de renseigner l'option scope
avec un objet a une conséquence très importante. Cela crée un scope isolé pour la directive, qui n'hérite pas d'un scope parent.
Autrement dit, on casse la chaîne d'héritage des scopes. C'est le meilleur moyen de découpler la directive du reste de l'application.
La bonne pratique est de TOUJOURS définir un scope isolé, sauf cas particulier
Il existe trois manières de passer un attribut à un scope :
En tant que String : @
En tant qu'objet : =
En tant qu'expression : &
Attribut par scope
Depuis la version 1.4, on peut attacher ces données directement a l'alias du controller grace au parametre "bindToController"
Attribut par scope - @
angular.module('app', [])
.directive('myShadow',myShadowDirective);
function myShadowDirective(){
return {
restrict: 'A',
scope: {
shadowColor: '@'
},
link: function(scope,element,attrs){
attrs.$observe('shadowColor',function(){
element.css('box-shadow','4px 4px 4px '+scope.shadowColor);
})
}
}
}
<section ng-app="app">
<p my-shadow shadow-color="blue">
lorem ipsum...
</p>
</section>
Attribut par scope (=)
angular.module('app', [])
.controller('DemoController',DemoController)
.directive('myShadow',myShadowDirective);
function DemoController(){
this.shadowConfig = {
width: 2,
height: 2,
deep: 2,
color: '#d01616'
}
}
function myShadowDirective(){
return {
restrict: 'A',
scope: {
shadowConfig: '='
},
link: function(scope,element,attrs){
attrs.$observe('shadowConfig',function(){
var conf = scope.shadowConfig;
var shadowStr = conf.width+'px '+conf.height+'px '+conf.deep+'px '+conf.color;
element.css('box-shadow',shadowStr);
})
}
}
}
<section ng-app="app" ng-controller="DemoController as ctrl">
<p my-shadow shadow-config="ctrl.shadowConfig">
lorem ipsum
</p>
</section>
Attribut par scope (&)
angular.module('app',[])
.directive('demo',demoDirective)
.controller('DemoController',DemoController);
function demoDirective(){
return {
template: '<div><span>Hello </span>'+
'<span ng-click="demoCtrl.worldClicked()">World</span>'+
'</div>',
scope:{},
controller: function(){
var self = this;
this.worldClicked = function(){
self.demoCtrl.clicked('hello !!!');
}
},
controllerAs: 'demoCtrl',
bindToCtrl: {
clicked: '&'
}
}
}
function DemoController(){
this.direciveClicked = function(message){
alert(message);
}
}
<section ng-app="app" ng-controller="DemoController as ctrl">
<div demo clicked="ctrl.directiveClicked"></div>
</section>
Transclusion
angular.module('app',[])
.directive('demo',demoDirective);
function demoDirective(){
return {
transclude: true,
template: '<article><p>Hello </p>'+
'<p ng-transclude></p>'+
'</article>'
}
}
<section ng-app="app">
<h1>Demo !!!!</h1>
<demo>
<div>
I will be included in ng-transclude DOM's node
</div>
</demo>
</section>
La transclusion permet d'insérer un peu de HTML à un endroit précis du template de la directive
Intéraction entre directives
angular.module('app',[])
.directive('demo',demoDirective);
function demoDirective(){
return {
restrict: 'E',
require: 'ngModel',
link: function(scope,element,attrs,ngModelCtrl){
//we can manipulate ngModel's value
}
}
}
<section ng-app="app">
<demo ng-model="test.value">
</demo>
</section>
Afin de créer des composants complexes, il est possible d' imposer l'utilisation conjointe de plusieurs directives.
Il faut pour cela utiliser l'attribut require dans la definition d'une directive.
$observe
$observe fonctionne de la même manière que $watch.
$observe n'est utilisable que dans une directive, en effet $observe ne peut être utilisé qu'avec le DOM.
Il faut toujours privilégier l'utilisation de $observe par rapport à scope.$watch dans une directive.
TP - Etape 7
Consignes :
Corrigé :
$ git checkout step7-findcomment-solution
Communiquer efficacement avec une API RESTful
Back-end
Les données sont la plupart du temps fournies par une API RESTful
Il y a différentes façons de communiquer avec une API REST :
$http (integré)
$resource (module ngResource)
restangular (https://github.com/mgonto/restangular)
js-data-angular (https://github.com/js-data/js-data-angular)
Mais elle peuvent aussi venir d'un store local.
Pourquoi utiliser une librairie tierce pour communiquer avec le serveur ?
$http est un service relativement bas niveau
Les APIs RESTful sont relativement standardisées
On veut éviter les répétitions dans le code :
- Opérations CRUD
- Relations entre entités
- Cache
- Validation ?
TP - Etape 8
Consignes :
Corrigé :
$ git checkout step8-modelcache-solution
TP - Etape 9
Consignes :
Corrigé :
$ git checkout step9-deletecomment-solution
Validation de formulaire personnalisée
Depuis la version 1.3, il est possible de créer ses propres règles de validation .
$validators
$validators est un nouvel attribut de ngModel qui permet d'ajouter un ou plusieurs validateurs "custom" au modèle.
$validators attend en retour un booléen .
$asyncValidators
$asyncValidators attend en retour une promise (s'utilise très bien avec $http ).
Fonctionne de la même maniére que $validators mais pour les traitements asynchrones.
Ces deux techniques sont à utiliser dans une directive.
$pending
L'attribut $pending permet d'afficher un message ou un spinner en attendant la réponse du validateur asynchrone.
Dirty Checking
Les watchers
A chaque fois que l'on utilise ng-model, ng-repeat et d'autres, angular met en place des "watchers"
Un "watcher" est une simple fonction qui sera appelée par le framework pour mettre à jour le DOM
// Si l'on souhaite observer un attribut de $scope
$scope.$watch('title', function(newVal, oldVal) {
//faire quelque chose
});
// A utiliser si on souhaite observer autre chose que le scope
$scope.$watch(function() {
return myService.data.title;
}, function(newVal, oldVal) {
//faire quelque chose
});
// Un troisième argument de type booléen permet d'observer un objet en profondeur
$scope.$watch(function(){
return myService.data;
}, function(newVal, oldVal) {
// ici c'est tout l'objet myService.data qui est observé
},true);
Nous pouvons déclarer nos propres watchers...
function MyController($scope){
var self = this;
self.collection = [{
id: 1,
content: 'foo'
}];
$scope.$watchCollection(function() {
return self.collection;
}, function(newVal, oldVal) {
//faire quelque chose
});
}
angular.module('app',[])
.controller('MyController',MyController);
Mais egalement surveiller une collection (un array)
Depuis la version 1.3 watchGroup
La méthode $watchGroup de $scope permet de mettre en place plusieurs watchers d'un coup en specifiant un array d'expressions
//ici $scope.foo et $scope.bar seront observés
$scope.$watchGroup(['foo', 'bar'], function(newVal, oldVal, scope) {
//faire quelque chose
});
$scope.$watchGroup(function() {
return [
myService.data,
'foo'
]
}, function(newVal, oldVal, scope) {
//faire quelque chose
});
$digest et $apply
$scope.$digest() va exécuter tous les watchers liés au scope.
"Dirty checking"
$scope.$apply() va appeler $rootScope.$digest(), ce qui va exécuter tous les watchers.
$digest permet à angular de maintenir le modèle et la vue synchronisés.
Le framework lance $digest à chaque fois :
- qu'un bouton avec la directive ng-click est cliqué
- que la valeur d'un input bindé avec ng-model change
- qu'un callback asynchrone est exécuté...
Modules Complémentaires
Sécurité ngSanitize
Permet de sécuriser les saisies utilisateurs, afin d'éviter toute injection de code malicieux.
bower install ng-sanitize --save
angular.module('app',[
'ngSanitize'
])
<script src=""></script>
ngSanitize nous permet d'utiliser la directive ng-bind-html.
Le html sera parsé et tout élément dangereux supprimé
Pour outrepasser ngSanitize et injecter du html complexe, on peut utiliser le service $sce.
De plus le module fournit un filtre linky
Ce filtre permet de transformer toutes les URL d'une String en lien HTML (balise <a>)
Ce filtre s'utilise avec ng-bind ou ng-bind-html.
animation ngAnimate
Le module ngAnimate permet d'utiliser directement des transitions et keyframes CSS3.
bower install ng-animate --save
angular.module('app',[
'ngAnimate'
])
<script src=""></script>
Le framework va tout simplement ajouter des classes CSS à nos éléments html quand ils entrent dans le DOM, ou juste avant d'en sortir.
-
.ng-enter
-
.ng-enter-active
-
.ng-leave
-
.ng-leave-active
Exercice
Modifier le fichier style.css afin d'animer la liste de posts dans le template list.tpl.html
l'animation fonctionnera conjointement avec le filtre
mobile ngTouch
Le module ngTouch fournit plusieurs directives adaptées aux écrans tactiles
bower install ng-touch --save
angular.module('app',[
'ngTouch'
])
<script src=""></script>
Homogénéisation de ngClick, le comportement sera le même sur mobile et desktop (délai de 300ms sur mobile)
Deux directives :
ngSwipeLeft ;
ngSwipeRight ;
Ces deux directives fonctionnent comme ngClick.
Tester une application AngularJS
Tests unitaires
Karma
Outil simplifiant l'exécution des tests
Permet de tester plusieurs navigateurs simultanément
Supporte plusieurs frameworks de test
Jasmine
Mocha
QUnit
...
Tests Fonctionnels (E2E)
Protractor
Framework de tests fonctionnels
Construit sur WebDriverJS (Selenium)
Permet de simuler l'intéraction d'un utilisateur avec le navigateur
"Black box testing"
Framework de test
Jasmine
Framework de test JavaScript
Fournit un DSL expressif
Syntaxe "Behavior Driven"
Framework par défaut pour Karma et Protractor
describe("A suite", function() {
it("contains spec with an expectation", function() {
expect(true).toBe(true);
});
});
ATDD
describe('myComponent', function() {
beforeEach(module('myModule'));
beforeEach(inject(...));
it('should do something', function() {
...
});
})
-
Les fonctions module() et inject() sont fournies par la librairie angular-mocks
-
module() permet de charger un module dans le contexte du test (ne pas confondre avec angular.module())
-
inject() permet d'obtenir les composants requis pour les tests
Tests unitaires - Squelette
describe('myService', function() {
var myService;
beforeEach(module('myModule'));
beforeEach(inject(function(_myService_) {
myService = _myService_;
}));
it('should return true', function() {
var result = myService.getTrue();
expect(result).toBe(true);
});
})
Tester un Service
inject() ignore les underscores du paramètre _myService_ , ce qui permet de créer une variable myService sans qu'elle soit masquée
describe('GreetingsCtrl', function() {
var greetingsCtrl;
beforeEach(module('app'));
beforeEach(inject(function($controller) {
greetingsCtrl = $controller('GreetingsCtrl');
}));
it('should say Hello', function() {
expect(greetingsCtrl.message).toBe("Hello");
});
})
angular.module('app', [])
.controller('GreetingsCtrl', function() {
this.message = "Hello"
})
Tester un Controller (sans $scope)
describe('GreetingsCtrl', function() {
var $scope, greetingsCtrl;
beforeEach(module('app'));
beforeEach(inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
greetingsCtrl = $controller('GreetingsCtrl', {$scope: $scope});
}));
it('should say Hello', function() {
expect($scope.message).toBe("Hello");
});
})
angular.module('app', [])
.controller('GreetingsCtrl', function($scope) {
$scope.message = "Hello"
})
Tester un Controller (avec $scope)
describe('limitTo', function() {
var limitTo;
beforeEach(module('app'));
beforeEach(inject(function($filter) {
limitTo = $filter('limitTo');
}));
it('should limit array to one element', function() {
var array = ['a', 'b', 'c'];
var result = limitTo(array, 1);
expect(result.length).toBe(1);
});
})
Tester un Filtre
describe('panel directive', function() {
var element;
beforeEach(module('app'));
beforeEach(inject(function($compile, $rootScope) {
element = angular.element('<div panel>Some text</div>');
$compile(element)($rootScope)
}));
it('should add class panel to element', function() {
expect(element.hasClass('panel')).toBe(true);
});
})
angular.module('app', [])
.directive('panel', function() {
return function(scope, element) {
element.addClass('panel');
}
})
Tester une Directive
describe('Post model tests', function () {
var myService,
$httpBackend,
url = 'http://my-web-service.com/posts';
beforeEach(module('app'));
beforeEach(inject(function(_myService_, _$httpBackend_){
myService = _myService_;
$httpBackend = _$httpBackend_;
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should create an post', function () {
var postData = {
content: 'a content'
};
$httpBackend.whenPOST(url).respond(postData);
var createdPost;
myService
.create(postData)
.success(function(data){
createdPost = data;
});
$httpBackend.expectPOST(url, postData);
$httpBackend.flush();
expect(createdPost).toBeDefined();
});
});
Tester un service utilisant $http
angular.module('app',[])
.factory('Post',function($http){
var url = '';
return {
create : function(post){
return $http.post(url,post);
}
}
})
sudo npm install -g protractor
Tests fonctionnels (E2E)
sudo webdriver-manager update
sudo webdriver-manager start
Installation de selenium
Démarage de selenium
describe('Home screen', function () {
beforeEach(function() {
browser.get('/#');
});
it('should have title Home', function () {
expect(browser.getTitle()).toBe('Home');
});
});
Tests fonctionnels (E2E)
La fonction browser.get() permet d'envoyer le navigateur à l'URL de l'écran que l'on veut tester.
describe('Save screen', function () {
beforeEach(function() {
browser.get('/#');
});
it('should display message when save button clicked', function () {
var button = element(by.id('save-button'));
var messageElement = element(by.binding('saveMessage'));
button.click();
expect(messageElement.getText()).toBe('Saved');
});
})
<button id="save-button" ng-click="saveMessage='Saved'">Save</button>
<div>{{ saveMessage }}</div>
Liste des sélecteurs : https://github.com/angular/protractor/blob/master/docs/api.md#element
Simuler le comportement utilisateur
Problème
Les tests contiennent beaucoup de code de manipulation du DOM, ce qui les rendent :
-
Peu expressifs
-
Fragiles
Le code des tests est encombré de détails concernant le DOM, alors qu'il devrait s'intéresser uniquement aux règles métier .
De subtiles modifications du DOM peuvent rendre de nombreux tests inopérants, nécessitant un lourd travail de maintenance des tests
Page Object
Il est possible de créer des objets JavaScript qui fournissent une API permettant de manipuler chaque vue à tester.
Ces objets encapsulent le code de manipulation du DOM, cachant ainsi les détails d'implémentation
Si le DOM change, seul le Page Object doit être mis à jour
L'API fournie est à un niveau d'abstraction élevé, adapté à l'écriture et à la lecture des tests fonctionnels
Page Object - Exemple
function SavePage() {
this.saveButton = element(by.id('save-button'));
this.saveMessageElement = element(by.binding('saveMessage'));
this.get = function () {
browser.get('/#');
};
this.getSaveMessage = function () {
return this.saveMessageElement.getText();
}
}
module.exports = SavePage;
Tester avec un Page Object
var SavePage = require('./SavePage');
describe('Save page', function () {
var page = new SavePage();
beforeEach(function() {
page.get();
});
it('should display message when save button clicked', function () {
page.saveButton.click();
expect(page.getSaveMessage()).toBe('Saved');
});
})
End !
Et après ?
Support, exemples et exercices conçus par Mikael Couzic et Charles Jacquin
Annexes
$resource
Niveau d'abstraction plus élevé que $http.
Nécessite d'importer le package ngResource.
Avantages :
Très simple à mettre en place, quelques lignes suffisent
Inconvénients :
Documentation illisible
Pas de promise jusqu'à la version 1.2
Fonctionnement
angular.module('demo', ['ngResource'])
.factory('user',function($resource){
return $resource('/user/:id', {id:'@_id'});
});
L'objet (optionnel) passé en paramètre permet d'attribuer des valeurs par defaut aux paramètres de la route.
Prefixer d'un @ : $resource ira chercher la valeur de l'attribut correspondant dans l'objet sur laquelle l'action est appelée
Manipulations de base
$resource nous renvoie un objet disposant de 5 methodes :
get : récupère un objet
save : persiste un objet
query : retourne toute la collection
remove : supprime un enregistrement
delete : alias de remove
angular.module('demo', ['ngResource'])
.factory('user', function($resource) {
return $resource('http://my-web-service.org/user/:id', {id:'@_id'});
});
.controller('UserController', function(user) {
this.save = function(user) {
user
.save(this)
.$promise
.then(function(user) {
//do something
})
.catch(function(err) {
//do something
})
}
this.remove = function() {
user
.remove()
.$promise
.then(function() {
//do something
})
}
this.getOne = function() {
user
.get()
.$promise
.then(function(user) {
//do something
})
}
this.getAll = function() {
user
.query()
.$promise
.then(function(users) {
//do something
})
}
});
Méthodes personnalisées
Au moment de la création de la ressource, il est possible de rajouter des méthodes personnalisées, comme par exemple la methode update qui est absente.
angular.module('demo',[
'ngResource'
])
.factory('userModel',function($resource){
return $resource('http://my-web-service.org/user/:id', {id:'@_id'},{
'update': { method:'PUT' },
'follow' : {
method : 'Post',
url : 'http://my-web-service.org/user/:id/like'
}
});
})
.controller('UserController',function(){
var self = this;
this.save = function(user){
if(user._id){
user
.save()
.$promise
.then(function(me){
self.me = me;
})
}else{
user
.update()
.$promise.then(function(me){
self.me = me
})
}
}
});
js-data
"Respect your data"
"Give your data the treatment it deserves with a data store built for AngularJS."
bower install --save js-data js-data-angular
Dernier arrivé et inspiré par Ember.js, angular-data corrige les fautes de ses prédécesseurs.
Permet avec une API unique de persister tout aussi bien à distance (http), que localement (indexedDB). Il suffira de préciser un "adapter".
Très bonne gestion du cache, particulièrement adapté aux fonctionnalités en temps réel (WebSocket, Server push...)
Compatible avec Node.js (en utilisant l'adapter MongoDB par exemple)
Data-store
A la base simple service de stockage en mémoire, la librairie a évolué et propose desormais bien plus.
Memory and persistent
Tout ce qui est récupéré via l'adapter spécifié (http par défaut) et correspondant au model est stocké en mémoire cache.
L'api expose deux méthodes pour chaque operations CRUD, une pour la mémoire cache, l'autre via l'adapter
Définir une resource
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api'
});
});
name définit le nom de la resource
idAttribute fait office de clef primaire
endpoint fait reference aux point d'entrée dans l'API REST
baseUrl est l'url de base de notre API REST
Définir un adapter
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
defaultAdapter: 'DSLocalForageAdapter'
});
});
Ici on indique que toutes les données seront persistées par défaut localement en utilisant localForage, une librairie développée par Mozilla qui standardise le localStorage
Nous aurons la possiblité plus tard de changer d'adapter
2 types de méthodes :
-
synchrone
-
asynchrone (retourne une promise)
Les méthodes synchrones vont récupérer les données depuis le cache
Les méthodes asynchrones vont quant à elles recupérer les données en utilisant l'adapter (http par default)
angular.module('app',[])
.controller('UserFormController', function(User) {
// this.submit est juste la pour l'exemple
this.submit = function(userData){
// une requête HTTP POST est faite (en fonction du paramétrage de User)
User.create(userData).then(function(createdUser) {
})
}
})
Création via l'adapter
Ou dans le "memory store"
angular.module('app')
.controller('UserController', function(User){
var user = {
id: 10,
name: 'MrFoo'
};
// l'objet n'est pas persisté via l'adapter, mais uniquement stocké en cache
User.inject(user);
})
Le C de CRUD
angular.module('app',[])
.controller('UsersController', function(User){
User.getAll(); //retourne undefined
User.findAll().then(function(users) {
// users est un array d'instances de la ressource user
User.getAll(); // retourne un array d'instances de la ressource user
})
})
Récupérer des données via l'adapter
Ou dans le "memory store"
angular.module('app')
.controller('UserController', function(User) {
User.get(5); // retourne undefined
User.find(5).then(function(users) {
// users est une instance de la ressource user
User.get(5); // retourne une instance de la ressource user
})
})
Le R de CRUD
angular.module('app',[])
.controller('UsersController', function(User) {
var myUser = User.get(5);
myUser.name = 'MrBar';
User.save().then(function(user) {
//user est l'objet mis à jour
});
})
Mettre à jour des données via l'adapter
Le U de CRUD
angular.module('app',[])
.controller('UsersController', function(User) {
User.destroy(5).then(function() {
User.get(5) // retourne undefined
});
})
Supprimer des données via l'adapter
Le D de CRUD
angular.module('app',[])
.controller('UsersController',function(User){
User.findAll( { adapter: 'DSLocalForageAdapter' } ).then(function(){
});
})
Specifier un adapter differend de celui par default
Validation
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
beforeValidate: function (resourceName, attrs, cb) {
//faites quelque chose avant la validation
},
validate: function (resourceName, attrs, cb) {
if (!attrs.firstName) {
cb('Title is required');
} else {
cb(null, attrs);
}
},
afterValidate: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
}
});
L'attribut validate permet de définir une fonction de validation.
beforeValidate et afterValidate seront executé avant et aprés la validation.
Intercepteurs
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
beforeCreate: function (resourceName, attrs, cb) {
//faites quelque chose avant la validation
},
beforeInject: function (resourceName, attrs, cb) {
//faites quelque chose avant la validation
},
beforeUpdate: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
}
beforeDestroy: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
}
});
De la même manière nous allons pouvoir spécifier des callback qui seront appelés à des moments clef de la vie nos objets.
Intercepteurs
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
afterCreate: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
},
afterInject: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
},
afterUpdate: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
}
afterDestroy: function (resourceName, attrs, cb) {
//faites quelque chose aprés la validation
}
});
La même chose mais après l'action.
Sérialisation/Désérialisation
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
serialize: function serialize(resourceName, data) {
return {
payload: data
}; },
deserialize: function deserialize(resourceName, data) {
return {
payload: data
};
}
});
serialize sera appelée juste avant que les données soient envoyées à l'adapter
deserialize est appelée après que des données aient été reçues de l'adapter
Pratique si le web service renvoie les données dans un format non standard
Méthodes personnalisées
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
methods: {
fullName: function () {
return this.firstName + ' ' + this.lastName;
}
}
}
});
Toute méthode définie dans l'attribut methods sera ajoutée au prototype des instances de notre ressource
Méthodes statiques
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
var User = DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
methods: {
fullName: function () {
return this.firstName + ' ' + this.lastName;
}
}
}
User.aStaticMethod = function(){
}
return User;
});
Relations entre entités
var app = angular.module('myApp', ['js-data']);
app.factory('User', function (DS) {
return DS.defineResource({
name: 'user',
idAttribute: '_id',
endpoint: 'users',
baseUrl: 'https://example.com/api',
relations: {
hasMany: {
comment: {
localField: 'comments',
foreignKey: 'userId'
}
},
hasOne: {
profile: {
localField: 'profile',
foreignKey: 'userId'
}
},
belongsTo: {
organization: {
localKey: 'organizationId',
localField: 'organization',
// impacte la structure de l'url /organization/15/user/4
parent: true
}
}
});
Récupérer les données des relations
angular.module('app')
.controller('UserController',function(User){
User.find(5).then(function(user){
//user est une instance de la resource user
DS.loadRelations( user, ['comment', 'profile']).then(function (user) {
user.comments;
user.profile;
});
})
})
Bind dans un controlleur avec $scope
angular.module('app')
.controller('UserController',function(User,$scope){
User.bindOne($scope, 'me', 1);
User.bindAll($scope, 'friends');
})
Bind dans un controlleur sans $scope
angular.module('app')
.controller('UserController',function(User,$scope){
var self = this;
$scope.$watch(function () {
return User.lastModified($stateParams.id);
}, function () {
self.me = User.get($stateParams.id);
});
$scope.$watch(function () {
// Changes when anything in the User collection is modified
return User.lastModified();
}, function () {
self.friends = User.filter(query);
});
})
Authentification Satellizer
Module qui simplifie grandement l'authentification.
bower install satellizer --save
angular.module('app',[
'satellizer'
])
<script src=""></script>
AngularJS 1.4
By AdapTeach
AngularJS 1.4
- 3,541