AngularJS 1.5
Table des matières
- Introduction
- Les composants du framework
- Nouveautés de la version 1.5
- Environnement de développement
- Principales directives
- NgMaterial
- UiRouter
- Validations de formulaires
- Les services
- Gestion du modèle
- Modules complémentaires
- Tests unitaires et fonctionnels
Application Web centrée SERVEUR
Data-binding traditionnel
Application Web centrée CLIENT
Single Page Application (SPA)
Historique du 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 JavaScript
- Backbone.js (2010)
- Ember.js (2011)
- AngularJS (2012)
- React.js (2013)
- Vue.js (2014)
Et en 2016...
Et demain ?
"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
Data Binding par AngularJS
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-model permet de data-binder le modèle à la vue
{{ }} permet d'évaluer une expression
En résumé...
Cool ! mais en pratique...
AngularJS & le développeur jQuery
Data Binding manuel avec 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);
});
});
Approches radicalement différents
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.
Et pourtant...
...AngularJS manipule le DOM...
... or, jQuery offre une API unique pour manipuler le DOM, et gomme les différences entre différents navigateurs
...alors, jQuery ou pas jQuery ?
Dans le cas où jQuery est chargé avant AngularJS, c'est jQuery qui sera utilisé pour la manipulation du DOM
Dans le cas contraire, AngularJS utilisera sa propre librairie interne appelée jqLite, qui fournit un sous-ensemble essentiel des fonctionnalités de jQuery
jqLite !!!
Les Composants
- Modules
- Templates (vue)
- Controllers
- Directives
- Filters
- Components
- Services
- angular.module
- angular.copy
- angular.isDefined
- angular.equals
- angular.extend
- angular.merge
- angular.forEach
- angular.is...
- angular.lowercase
- angular.uppercase
- angular.noop
- angular.version
L'objet Angular
Les Modules
Les modules permettent de découper l'application.
Une application AngularJS 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.
Dépendances
Au lancement de l'application, toutes les dépendances (directes ou indirectes) 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, deux services, deux directives... 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 !
Bonnes pratiques
Chaque route (url) est un module
Cela permet à chaque route d'être découplée du reste de l'application, elle peut ainsi être facilement déplacée, voire réutilisée dans une autre application
Refléter l'arborescence des dossiers
Dans le code source de l'application, chaque dossier définit son propre module et déclare comme dépendances les modules définis dans les sous-dossiers
Templates (vue)
- Fragments de HTML réutilisables
- Compilés par AngularJS
- Les vues générées sont dynamiques (Data Binding)
Templates
<section>
some content
</section>
<script type="text/ng-template" id="myDialog.html">
</script>
On peut également déclarer un template imbriqué dans un autre template .
Controllers
<section>
<section ng-controller="MyFirstCtrl">
...
</section>
<section ng-controller="MySecondCtrl as ctrl">
...
</section>
</section>
ng-controller (déprécié)
Directive permettant de lier un controlleur à un element du DOM
Controllers avec $scope (déprécié)
<section ng-app="app">
<h1 ng-controller="HelloWorldController">
{{ message }}
</h1>
</section>
message est une propriété du scope
$scope est un service injecté dans le controlleur par le framework
class HelloWorldController { // ES6
constructor($scope) {
$scope.message = 'Hello, World !'
}
}
angular.module('app', [])
.controller('HelloWorldController', HelloWorldController)
Controller ES5 (déprécié)
<section ng-app="app">
<h1 ng-controller="HelloWorldController">
{{ message }}
</h1>
</section>
Le template est identique à la version ES6
Le controller est une simple fonction JavaScript
function HelloWorldController($scope) {
$scope.message = 'Hello, World !'
}
angular.module('app', [])
.controller('HelloWorldController', HelloWorldController)
Scopes (déprécié)
Héritage de Scopes (déprécié)
Héritage de Scopes (déprécié)
- 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é)
Propagation d'événement (déprécié)
"Controller as" (déprécié)
<section ng-app="app">
<h1 ng-controller="HelloWorldController as ctrl">
{{ ctrl.message }}
</h1>
</section>
ctrl référence l'instance du Controller
class HelloWorldController {
constructor() {
this.message = 'Hello, World!'
}
}
angular.module('app', [])
.controller('HelloWorldController', HelloWorldController)
message est une propriété du controller
"Controller as" imbriqués (déprecié)
<div ng-app="app">
<div ng-controller="ParentController as parent">
<div ng-controller="ChildController as child">
<div>{{ parent.message }}</div>
<div>{{ child.message }}</div>
</div>
</div>
</div>
class ParentController {
constructor() {
this.message = 'Parent Controller Property'
}
}
class ChildController {
constructor() {
this.message = 'Child Controller Property'
}
}
angular.module('app', [])
.controller('ParentController', ParentController)
.controller('ChildController', ChildController)
Exercice : "Controller as"
- 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 (déprécié)
- Décoration du DOM
- Utilisées comme des extensions à HTML
- Encapsule les manipulations du DOM
- Faciles à utiliser, difficiles à créer...
Principales Directives fournies
- ng-app
- ng-controller
- ng-model
- ng-bind
- ng-click
- ng-if
- ng-show
- ng-hide
- ng-cloak
- ng-class
- ng-href
- ng-src
- ng-repeat
- ng-transclude
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>
class MyController {
logIn() {
alert('Logged In')
}
}
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 class="panel">ctrl: {{ ctrl }}</div>
<div ng-if="!ctrl.isLogged" class="panel">
<p>Please Login</p>
<button ng-click="ctrl.isLogged = true">Login</button>
</div>
<div ng-if="ctrl.isLogged" class="panel">
<p>Welcome !</p>
<button ng-click="ctrl.isLogged = false">Logout</button>
</div>
</div>
class LoginController {
constructor() {
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" ng-controller="MyController as ctrl">
<div class="panel">ctrl: {{ ctrl }}</div>
<div class="panel">
<button ng-click="ctrl.value = !ctrl.value">Toggle class</button>
<p ng-class="{ 'my-class' : ctrl.value }">lorem ipsum blablabla ....</p>
</div>
</section>
class MyController {
constructor() {
this.value = false;
}
}
angular.module('app', [])
.controller('MyController', MyController)
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>
class MyController {
constructor() {
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 les types :
- 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"
Instructions :
ng-cloak
Permet d'éviter un "flickering effect" qui peut avoir lieu lorsque le navigateur affiche les templates dans leur version non-compilée, avant que AngularJS n'ait eu le temps de s'initialiser.
Pour qu'elle fonctionne, le script AngularJS doit être chargé dans la balise <head> du document HTML.
<div ng-app="app" ng-cloak>
...
</div>
Utilisés dans les templates, un Filter permet d'afficher une version transformée du modèle (filtrée, formatée, triée...)
Filters
- filter
- currency
- number
- date
- json
- lowercase
- uppercase
- limitTo
- orderBy
Plusieurs Filters peuvent être appliqués à la suite, à la façon des "pipe" UNIX
Filters == Pipes
Dans Angular 2, ils ont été renommés Pipes
Le Filter "filter"
<label>First Name</label>
<input type="text" ng-model="search.firstName">
<label>Last Name</label>
<input type="text" ng-model="search.lastName">
<ul>
<li ng-repeat="person in ctrl.people | filter:search">
{{ person.firstName }} {{ person.lastName }}
</li>
</ul>
.component()
Depuis la version 1.5
Design Pattern: Composite
class GreeterController() {
constructor() {
this.name = 'Jon Snow'
}
}
angular.module('app', [])
.component('greeter', {
template: '<div>Hello ,{{$ctrl.name}}</div>',
controller: GreeterController
})
- Nouveauté de la version 1.5
- Rétro-porté sur la 1.3 par Todd Motto
- Rend obsolètes ngController et les "template directives"
- Facilite la migration vers Angular 2
<greeter></greeter>
angular.module('app', [])
.component('panel', {
template: `
<div>
This is a Panel:
<div ng-transclude></div>
</div>`,
transclude: true
})
<panel>
Some content
</panel>
Transclusion
Permet de créer des composants "décorateurs"
Stateless VS Stateful components
On applique le Single Responsibility Principle (SRP) en séparant les composants en deux types:
Stateless
- Responsable de l'affichage des données
- Toutes ses données lui sont fournies par le composant parent
- N'interprète pas les actions de l'utilisateur
- "Dumb component"
- "Presentational component"
Stateful
- Responsable d'aller récupérer les données
- Délègue l'affichage à des composants Stateless
- Interprète les actions de l'utilisateur
- "Smart component"
- "Connected component"
Stateless Component Theory
- Reçoit des données : INPUTS
- Émet des événements : OUTPUTS
- Ne sais pas d'où vient la donnée
- N'interprète pas actions de l'utilisateur, mais se contente de les remonter au composant parent via ses outputs
- Sa principale responsabilité est d'afficher des données => "Presentational Component"
- Ne doit posséder aucune logique métier => "Dumb Component"
angular.module('app', [])
.component('greeter', {
template: '<div>Hello ,{{$ctrl.name}}</div>',
bindings: {
name: '@'
}
})
- Un "Stateless component" reçoit toutes ses données par son composant parent.
- Ne dépend que de ses "Inputs", définis dans les bindings
- Le composant réutilisable par excellence
- Le controller est optionnel
<greeter name="Jon Snow"></greeter>
Stateless Component Inputs
angular.module('app', [])
.component('ui', {
template: `
<item name="Leather Bag"
on-add-to-cart="$ctrl.addToCart(name)">
</item>`,
controller: UiController
})
.component('item', {
template: `
<h3>{{$ctrl.name}}</h3>
<button ng-click="$ctrl.onAddToCart({name: $ctrl.name})">
Add to Cart
</button>`,
bindings: {
name: '@',
onAddToCart: '&'
}
})
Stateless Component Outputs
INPUTS : Bonnes Pratiques
- N'utiliser que les bindings "<" et "@"
- Ne pas modifier les instances passées en inputs
- Si besoin, copier les inputs passés par binding uni-directionnel dans le $onChanges() du controller:
class SomeComponentController {
$onChanges() {
this.someInput = angular.copy(this.someInput)
}
}
ng-model : Bonnes pratiques
Le Data Binding bi-directionnel peut créer des effets de bord non souhaités.
Pour les éviter, il faut contraindre l'utilisation de ng-model.
Cette directive ne doit être utilisée que pour data-binder des valeurs locales, encapsulées dans un composant, donc invisibles en dehors du composant.
Pour satisfaire cette contrainte, on appliquera donc la même technique que précédemment : la copie d'input
class SomeComponentController {
$onChanges() {
this.someInput = angular.copy(this.someInput)
}
}
<input type="text" ng-model="$ctrl.someInput.someValue">
Stateful Component Theory
- Responsable de la récupération des données à afficher => "Connected component"
- Ecoute et gère les évènements remontés par les composants "Stateless", centralise l'interprétation des actions utilisateur => "Smart component"
- Délègue l'affichage à des composants "Stateless" => "Container component"
Bonnes pratiques d'architecture
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
Services
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){
setTimeout(function() {
data++;
callback(data);
},1000)
}
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
class MyController {
constructor($q, $timeout) {
this.$q = $q
this.$timeout = $timeout
}
asyncFunction() {
const promise = this.$q((resolve, reject) => {
this.$timeout(() => {
if (Math.random() < 0.5) {
resolve('successfull');
} else {
reject(new Error('not successfull :('));
}
}, 1000);
})
promise
.then(result => console.log(result))
.catch(error => console.error(error.message))
}
}
angular.module('app', [])
.controller('MyController', MyController)
function doAsyncCall(data) {
return $q(resolve => {
$timeout(() => resolve(++data), 1000)
})
}
var data = 1;
doAsyncCall(data)
.then(doAsyncCall)
.then(doAsyncCall)
.then(finalData => console.log(finalData)) //affiche 4
.catch(err => console.error(err))
Chainer des promises
function doAsyncCall(data) {
return $q(resolve => resolve(data++))
}
var data = 1
var anotherData = 2
$q.all([
doAsyncCall(data),
doAsyncCall(anotherData),
anotherPromise
]).then(data => {
//data est un tableau de taille 3
}).catch(err => {
// Handle error
})
Traitements parallèles
$http
Le service $http va nous permettre d'effectuer des appels AJAX et de traiter les réponses à l'aide des Promises
$http({
method : 'GET',
url : '/users',
headers : {
Authorization: 'Bearer khksjhfkjshfjkhshfkjshfuhuhgi'
}
}).then(response => {
//do something with response
}).catch(err => {
//something wrong happens
})
$http.get('/login',{
params : { //send an http request to /login?user=foo
user : 'foo'
}
}).then(response => {
// do something with the server's response
}).catch(err => {
//something wrong happens
})
GET
Permet d'envoyer une requête GET vers une url.
$http.post('/users',{
pseudo : 'foo',
password : 'bar'
}).then(response => {
//do something with server response
}).catch(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')
.then(result => {
//do something with result
}).catch(err => {
//something wrong happens
})
$http.head('/users/01')
.then(result => {
//do something with result
}).catch(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(someServiceProvider => { ... })
angular.module('myModule', [])
.run(someService => { ... })
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
class MyService {
constructor() {
this.name = 'My Service Name'
this.purpose = 'For demo only'
}
sayHello(name) {
return 'Hello, ' + this.name
}
}
angular.module('app', [])
.service('MyService', MyService)
AngularJS se charge de créer l'instance unique du service et de l'injecter au besoin
module.service() - ES5
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);
Utilisée avec ECMAScript 5, la méthode service() prend en paramètre une simple fonction
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 un Service.
- Appeler les méthodes du Service dans le Controller.
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(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 helloWorldProvider() {
this.name = 'Default'
this.$get = function() {
var name = this.name
return {
sayHello: function() {
return "Hello, " + name + "!"
}
}
}
this.setName = function(name) {
this.name = name;
}
}
angular.module('app', [])
.provider('helloWorld', helloWorldProvider)
.config(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
Node.js & npm
Node.js est utilisé comme plateforme de développement.
npm est le gestionnaire de paquets pour Node.js, et permet d'installer les outils de l'environnement de développement ainsi que les dépendances de notre projet.
$ npm search nom-du-module
$ npm install nom-du-module
$ sudo npm install nom-du-module -g
$ npm install
$ npm update
Yeoman
Générateur d'application.
Yeoman va vous permettre de generer tout type de projet.
De nombreux generateurs sont disponible sur github.
$ npm install yo -g
Outil de gestion de dépendances front-end, très similaire à npm
$ sudo npm install -g bower
$ 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
webpack
-
production
-
unbiased
-
flexible
-
extensible
-
open source
The
module bundler.
Webpack va prendre en charge le build et le développement de A à Z.
Installation
$ npm i webpack webpack-dev-server -D
Une fois l'installation terminée, nous pouvons ajouter ces deux scripts à notre package.json :
{
"scripts": {
"build:prod": "webpack -p --progress",
"dev:server": "webpack-dev-server --inline --progress"
}
}
webpack.config.js
module.exports = {
debug: true,
cache: true,
devtool: 'cheap-module-eval-source-map',
entry: {
vendor: './src/vendor.js',
main: './src/app.js',
},
output: {
path: 'dist',
filename: '[name].bundle.js',
sourceMapFilename: '[name].map',
chunkFilename: '[id].chunk.js'
},
plugins: [
new DefinePlugin({
'ENV': JSON.stringify(process.env.ENV),
}),
],
module: {
loaders: [
{ test: /\.js$/, loader: 'babel-loader' },
{ test: /\.css$/, loader: 'style!css' },
{ test: /\.html$/, loader: 'raw-loader' },
]
}
});
$ 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
UI-Router 1.0.0
npm install --save angular-ui-router@next
import uiRouter from 'angular-ui-router'
angular.module('app', [uiRouter])
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'à ce que le router Angular 2 (Component Router) soit finalisé et rétro-porté sur AngularJS.
UI Router - API
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 ou component
-
On peut spécifier un controlleur à binder sur chaque template
-
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
State avec template
angular.module('myModule', ['ui.router'])
.config(function ($stateProvider) {
$stateProvider.state('home', {
url: '/',
templateUrl: 'home.tpl.html',
controller: 'HomeCtrl'
});
});
State avec component
angular.module('myModule', ['ui.router'])
.component('home', {
template: '<h1>Home</h1>'
})
.config(function ($stateProvider) {
$stateProvider.state('home', {
url: '/',
component: 'home'
})
})
Plusieurs vues dans un state
<body>
<section ui-view="main"></section>
<section ui-view="adBanner"></section>
</body>
$stateProvider.state('home', {
url: '/',
views: {
main: {
component: 'home'
},
adBanner: {
component: 'adBanner'
}
}
});
Routes imbriquées
.config(function ($stateProvider) {
$stateProvider.state('users', {
url: '/users',
component: 'users'
})
// Navigable via /users/list
$stateProvider.state('users.list', {
url: '/list',
component: 'usersList'
})
})
<body>
<div ui-view></div>
</body>
index.html
<h2>Users</h2>
<div ui-view></div>
users.html
Routes abstraites
.config(function ($stateProvider) {
// abstract, donc non-naviguable
$stateProvider.state('users', {
abstract: true,
url: '/users',
component: 'users'
})
// Navigable via /users/list
$stateProvider.state('users.list', {
url: '/list',
component: 'usersList'
})
})
<body>
<div ui-view></div>
</body>
index.html
<h2>Users</h2>
<div ui-view></div>
users.html
Ici l'utilisateur ne pourra pas se rendre à l'url /users
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 (controller)
$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;
})
resolve (component)
$stateProvider.state('user', {
url: '/user/:id',
component: 'user',
resolve: {
userData: ($http, $stateParams) => {
return $http.get('/users/' + $stateParams.id)
}
}
})
.component('user', {
template: '<h3>Name : {{ $ctrl.userData.name }}</h3>',
bindings: {
userData: '<'
}
})
Gestion des erreurs
Ui-Router n'applique pas par défaut de stratégie de gestion d'erreurs.
Si une erreur se produit pendant le resolve, par défaut elle sera ignorée silencieusement.
Pour remédier à ça :
angular.module('app', ['ui.router'])
.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 dy 'injecter n'importe quel service
$stateProvider.state('restricted', {
url: '/restricted',
onEnter: function(authService){
if(!authService.logged){
return false;
}
},
controller : 'RestrictedController as ctrl',
templateUrl : 'restricted.tpl.html'
})
Si la fonction retourne false, la naviguation est annulée
UI Router Showcase
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>
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.
Data Service avec cache
class PersonRepo {
constructor(personBackend) {
this.personBackend = personBackend
this.cache = {}
}
save(person) {
this.personBackend.post(person)
.then(saved => {
this.cache[saved.id] = saved
return saved
})
}
}
class PersonBackend {
constructor($q) {
this.$q = $q
}
post({firstName, lastName}) {
return this.$q(resolve => resolve(new Person(firstName, lastName)))
}
}
angular.module('app', [])
.service('personRepo', PersonRepo)
.service('personBackend', PersonBackend)
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)
Créer des directives complexes
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
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.
Modules Complémentaires
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
ng-material
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
TDD - Principe
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
...
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);
});
});
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(httpResponse){
createdPost = httpResponse.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);
}
}
})
End !
Et après ?
Support, exemples et exercices conçus par Mikael Couzic et Charles Jacquin
AngularJS 1.5 (initiation)
By AdapTeach
AngularJS 1.5 (initiation)
- 2,857