AngularJS

Mikael Couzic 

Charles Jacquin

Table des matières

  1. AngularJS in a nutshell
  2. Les composants
  3. Angular core
  4. Environnement de développement
  5. Principales directives
  6. NgMaterial
  7. UiRouter
  8. Validations de formulaires
  9. Les services
  10. Gestion du modèle
  11. Modules complémentaires
  12. 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."

Tim O’Reilley

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

  1. Créez un module "app" et utilisez-le dans la vue
  2. Créez un controlleur "MyController" au sein de votre module et utilisez-le dans la vue avec la syntaxe "controller as"
  3. 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

  1. 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

  1. 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 :

  1. Array
  2. Object

Au lieu du traditionnel for dans le code JavaScript, nous n'aurons ici qu'un simple attribut HTML

Exercice

  1. Utiliser la directive ng-repeat pour afficher toutes les informations stockées dans le tableau du controller
  2. Afficher l'image en premier puis un titre suivis d'un lien
  3. Ecrire une methode "removeFramework()" dans le controller, qui prend un number en parametre et supprime le framework correspondant dans le tableau frameworks
  4. 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

  1. Reprendre la fin de l'exercice précedent.
  2. 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


 
La méthode service permet d'utiliser un constructeur JavaScript standard, on va donc utiliser le mot clef this pour définir les propriétés du 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

  1. Encapsuler le code asynchrone dans une factory.
  2. 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 :

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>

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