"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
<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
<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'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.
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.
... or, jQuery offre une API unique pour manipuler le DOM, et gomme les différences entre différents navigateurs
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
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.
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.
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.
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
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 !
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
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
<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 .
<section>
<section ng-controller="MyFirstCtrl">
...
</section>
<section ng-controller="MySecondCtrl as ctrl">
...
</section>
</section>
Directive permettant de lier un controlleur à un element du DOM
<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)
<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)
<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
<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)
ng-model permet de "data binder" un champ de formulaire :
<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 {{ }}.
<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 :
Toutes ces directives fonctionnent sur le modèle de ng-click.
<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-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;
<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)
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)
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 :
Au lieu du traditionnel for dans le code JavaScript, nous n'aurons ici qu'un simple attribut HTML
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...)
Plusieurs Filters peuvent être appliqués à la suite, à la façon des "pipe" UNIX
Dans Angular 2, ils ont été renommés Pipes
<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>
Depuis la version 1.5
class GreeterController() {
constructor() {
this.name = 'Jon Snow'
}
}
angular.module('app', [])
.component('greeter', {
template: '<div>Hello ,{{$ctrl.name}}</div>',
controller: GreeterController
})
<greeter></greeter>
angular.module('app', [])
.component('panel', {
template: `
<div>
This is a Panel:
<div ng-transclude></div>
</div>`,
transclude: true
})
<panel>
Some content
</panel>
Permet de créer des composants "décorateurs"
On applique le Single Responsibility Principle (SRP) en séparant les composants en deux types:
angular.module('app', [])
.component('greeter', {
template: '<div>Hello ,{{$ctrl.name}}</div>',
bindings: {
name: '@'
}
})
<greeter name="Jon Snow"></greeter>
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: '&'
}
})
class SomeComponentController {
$onChanges() {
this.someInput = angular.copy(this.someInput)
}
}
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">
Nous allons pouvoir injecter ces différents services dans nos controllers, nos directives ou même nos propres services
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
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))
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
})
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
})
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
})
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
})
Delete
Head
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
Phase permettant de configurer les services en paramétrant leurs providers. Seuls les constantes et providers sont injectables.
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 => { ... })
Pour créer nos services nous disposons de 5 méthodes :
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
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
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.
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
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.
Peu utilisée, car les services déclarés ainsi ne peuvent pas dépendre d'autres services.
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(){}
}
}
})
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.
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
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
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.
$ sudo npm install gulp -g
production
unbiased
flexible
extensible
open source
The
module bundler.
Webpack va prendre en charge le build et le développement de A à Z.
$ 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"
}
}
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
npm install --save angular-ui-router@next
import uiRouter from 'angular-ui-router'
angular.module('app', [uiRouter])
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.
$state
$stateParams
ui-sref
ui-view
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 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
angular.module('myModule', ['ui.router'])
.config(function ($stateProvider) {
$stateProvider.state('home', {
url: '/',
templateUrl: 'home.tpl.html',
controller: 'HomeCtrl'
});
});
angular.module('myModule', ['ui.router'])
.component('home', {
template: '<h1>Home</h1>'
})
.config(function ($stateProvider) {
$stateProvider.state('home', {
url: '/',
component: 'home'
})
})
<body>
<section ui-view="main"></section>
<section ui-view="adBanner"></section>
</body>
$stateProvider.state('home', {
url: '/',
views: {
main: {
component: 'home'
},
adBanner: {
component: 'adBanner'
}
}
});
.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
.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 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 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>
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
$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;
})
$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: '<'
}
})
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);
})
})
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
Pas de JavaScript (ou presque)
S'appuie sur les validations de formulaire HTML5
Fonctionne grâce à 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
<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);
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.
<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>
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>
<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>
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)
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
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>
<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
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
:
Par défaut, la directive sera de type A (attribut)
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 :
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.
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.
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>
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"
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>
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 : &
Depuis la version 1.4, on peut attacher ces données directement a l'alias du controller grace au parametre "bindToController"
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>
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>
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>
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
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.
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.
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
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
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.
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 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
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);
});
})
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"
})
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"
})
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);
});
})
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');
}
})
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();
});
});
angular.module('app',[])
.factory('Post',function($http){
var url = '';
return {
create : function(post){
return $http.post(url,post);
}
}
})
Et après ?
Support, exemples et exercices conçus par Mikael Couzic et Charles Jacquin