jQuery/BackboneJS = impératif, le JS commande l'HTML
$('#mapBtn').on('click', function(event) {
// do something
});
AngularJS = déclaratif, le HTML commande le JS
$scope.showMap = function() {
// do something
}
impératif = difficile à organiser et maintenir
déclaratif = plus simple à lire et moins de code
Dans le code HTML, on a:
#id
et .class
.Aucun/moins de risque d'erreurs de:
Une partie du DOM sur laquelle on a mis l'attribut ng-app
:
..
Autre moyen de démarrer une application en passant par javascript:
..
angular.bootstrap(document.getElementById('divApp'), ['testApp']);
Possibilité de mettre plusieurs applications sur la même page.
Parse la partie du DOM et exécute les directives qu'il trouve en ajoutant des templates, exécutant du JS,..
scope
est la glue entre controlleur et vue.scope
est un simple objet JS sur lequel on ajoute des propriétés (valeurs et méthodes).controllers
.ngApp
, un $rootScope
est créé, puis chaque controlleur crée un scope
descendant, ce qui crée une arborescence.$scope
peut accéder à son parent grâce à $scope.$parent
.
Hello {{name}}!
- {{name}} from {{department}}
scope
hérite des propriétés et méthodes de ses parents grâce au prototypage.scopes
/ controllers
.scopes
.scope
à un autre.$emit
$broadcast
L'event
remonte la hiérarchie des scopes
jusqu'au $rootScope
et peut être intercepté par chacun d'entre eux.
$scope.$emit('player.events.playVideo', video);
http://www.frangular.com/
L'event
descend chaque branche de la hiérarchie des scopes
en partant du scope
émetteur et peut être intercepté par chacun d'entre eux.
$scope.$broadcast('player.events.playVideo', video)
http://www.frangular.com/
$scope.$on('player.events.playVideo', function(event, video) {
$scope.playingVideo = video;
});
http://www.frangular.com/
app.controller('MainController', function($scope, $rootScope) {
$scope.mainData = {
logs: ''
};
$scope.$on('eventEmitedName', function(event, data) {
$scope.mainData.logs = $scope.mainData.logs + '\nMainController - receive EVENT "' + event.name + '" with message = "' + data.message + '"';
});
$rootScope.$on('eventEmitedName', function(event, data) {
$scope.mainData.logs = $scope.mainData.logs + '\n$rootScope - receive EVENT "' + event.name + '" with message = "' + data.message + '"';
});
});
app.controller('ParentController', function($scope) {
$scope.parentData = {
message: 'messageAAA'
};
$scope.broadcastEvent = function() {
$scope.$broadcast('eventBroadcastedName', $scope.parentData);
};
$scope.$on('eventEmitedName', function(event, data) {
$scope.mainData.logs = $scope.mainData.logs + '\nParentController - receive EVENT "' + event.name + '" with message = "' + data.message + '"';
});
});
app.controller('ChildController', function($scope) {
$scope.childData = {
message: 'messageBBB'
};
$scope.emitEvent = function() {
$scope.$emit('eventEmitedName', $scope.childData);
};
$scope.$on('eventBroadcastedName', function(event, data) {
$scope.mainData.logs = $scope.mainData.logs + '\nChildController - receive EVENT "' + event.name + '" with message = "' + data.message + '"';
});
});
http://plnkr.co/edit/am6IDw?p=preview
Angular aussi utilise des events:
app.controller('PlayerCtrl', function($scope) {
$scope.$on('$destroy', function(event) {
// controlleur sur le point d'être détruit, dernière volonté?
});
});
Unidirectionnel avec ngBind
/ {{ }}
.
{{ user.firstname }}
Bidirectionnel avec ngModel
.
ngRoute
, ngAnimate
, ngSanitize
, ngMock
,..Déclaration de module:
angular.module('myapp', ['myapp.feature1'
'myapp.feature2',
'ng-route',
'ng-animate'
'ui-router']);
Récupération de module:
angular.module('myapp');
Tous les controlleurs
, directives
, filters
et services
sont déclarés sur un module.
Exemple:
angular.module('myapp', ['myapp.utils', 'myapp.gallery']);
angular.module('myapp').controller('HomeCtrl', function() {});
angular.module('myapp').service('myService', function() {});
angular.module('myapp.gallery', []);
angular.module('myapp.gallery').controller('GalleryCtrl', function() {});
angular.module('myapp.utils', []);
angular.module('myapp.utils').constant('myFilter', {});
angular.module('myapp.utils').filter('myFilter', function() {});
config()
puis des run()
.config()
et de run()
.controllers
, directives
, filtres
,..angular.module('myapp', []);
angular.module('myapp').config(function($httpProvider) {
// Injection des providers des services
// (objets servant en interne à créer les instances des services)
// et de les configurer avant leur instanciation
$httpProvider.defaults.headers.common['Access-Control-Max-Age'] = '1728000';
$httpProvider.defaults.headers.common['Accept'] = 'application/json, text/javascript';
$httpProvider.defaults.headers.common['Content-Type'] = 'application/json; charset=utf-8';
});
angular.module('myapp').run(function($http) {
// Injection des instances des services pour utilisation
$http.post('/someUrl', {msg:'appStart'});
});
controllers
, filtres
, services
,..controllers
.Il existe 5 méthodes pour déclarer un service mais dans tous les cas, ils s'injectent et s'utilisent de la même manière et il en existe toujours une seule instance.
En pratique,factory()
etvalue()
sont utilisés dans la plupart du cas.
app.factory('MyService', function($window) {
var scale = 1;
return {
getScreenWidth: function() {
return $window.innerWidth * scale;
},
getScreenHeight: function() {
return $window.innerHeight * scale;
}
};
});
app.controller('AppCtrl', function($scope, MyService) {
var _width = MyService.getScreenWidth();
});
object
ou bien de type primitif (booléen
, string
, number
).app.value('GoogleMapAPI_KEY', '5VUhR4kqvVvhXRH4BIAN')
app.value('VideoData', {
title: 'Ters Akan şelale!',
description: 'Bu işte bir yanlışlık var diye İngiltere',
url: 'http://www.dailymotion.com/video/x28i2n3_ters-akan-selale_news'
})
app.controller('AppCtrl', function($scope, VideoData) {
$scope.video = VideoData;
});
playerApp.value('Params', {
URL_API: 'https://api.dailymotion.com/videos',
URL_EMBED: 'http://www.dailymotion.com/embed/video',
VIDEOS_PER_PAGE: 12,
VIDEOS_AUTOPLAY: true,
});
playerApp.factory('Dailymotion', function($http, $sce, Params) {
var getApiUrl = function(terms, page) {
var api_params = '?fields=id,title,description,duration_formatted,created_time&sort=relevance';
var api_limit = '&limit=' + Params.VIDEOS_PER_PAGE;
var api_page = '&page=' + page;
var api_search = '&search=' + terms.split(' ').join('+');
var api_jsonp = '&callback=JSON_CALLBACK';
return Params.URL_API + api_params + api_limit + api_page + api_search + api_jsonp;
};
var Service = {
getResults: function(terms, page) {
return $http({
method: 'JSONP',
url: getApiUrl(terms, page),
cache: true,
transformResponse: function (data, headers) {
data.nbPages = Math.ceil(data.total / data.limit);
return data;
}
});
},
getIframeUrl: function(videoId) {
var url = Params.URL_EMBED + '/' + videoId + '?autoPlay=' + Params.VIDEOS_AUTOPLAY;
return $sce.trustAsResourceUrl(url);
}
};
return Service;
});
playerApp.controller('VideoPlayerModalCtrl', function($scope, $modalInstance, Dailymotion, video) {
$scope.video = video;
$scope.iframeUrl = Dailymotion.getIframeUrl(video.id);
$scope.close = function () {
$modalInstance.close();
};
});
$http
, $location
, $filter
, $window
, $timeout
, $interval
, $animate
, $rootScope
, $document
,..
Providers
associés pour les configurer au démarrage:$httpProvider
, $locationProvider
, $filterProvider
, $animateProvider
,..
app.filter('truncate', function() {
return function(str, nbChar) {
return str.substring(0, nbChar);
};
});
{{ description | truncate:10 }}
Possibilité de les cumuler:
{{ data | filterA:param1 | filterB | filterC:param1:param2 }}
app.controller('MainCtrl', function($scope, $filter) {
var longText = 'Trés long texte, trés long texte, trés long texte';
$scope.filteredText = $filter('truncate')(longText, 10);
});
app.controller('MainCtrl', function($scope, truncateFilter) {
var longText = 'Trés long texte, trés long texte, trés long texte';
$scope.filteredText = truncateFilter(longText, 10);
});
{{ today | date:'short' }}
{{ today | date:'yyyy' }}
{{ 789123.4567 | number:2 }}
{{ 123 | currency }}
{{ 'Vincent' | uppercase }}
{{ 'Vincent' | lowercase }}
{{ 'Je fais un essai' | limitTo: 2}}
{{ 'Je fais un essai' | limitTo: -5}}
{{ [1, 2, 3, 4] | limitTo: 2}}
{{ ['alpha', 'beta', 'gamma'] | filter:'e' }}
{{ [{'nom': 'Nabet', 'prenom':'Vincent'},
{'nom': 'Van Gogh', 'prenom':'Vincent'}] | filter:{'nom':'Nabet'}:true }}
{{ ['alpha', 'beta', 'gamma'] | filter:fourCharac }}
$scope.fourCharac = function(elem) { return elem.length === 4;}
$http
et $timeout
retournent une promesse.app.controller('AppController', function($scope, $timeout) {
var promise = $timeout(3000);
$scope.name = 'waiting promise resolve';
promise.then(function() {
$scope.name = 'resolved!';
});
});
http://plnkr.co/edit/sYinVP?p=preview
app.controller('GreetController', function($scope, $http) {
var promise = $http.get('https://api.github.com/users/bertrandg');
promise.then(function(response) {
$scope.user = response.data;
}, function(reason) {
$scope.error = reason;
});
$scope.loading = true;
promise.finally(function(){
$scope.loading = false;
});
});
http://plnkr.co/edit/7fbtsQ?p=preview
app.factory('MaPromesse', function($q, $timeout) {
var Service = {
get: function() {
var deferred = $q.defer();
$timeout(3000).then(function() {
//deferred.resolve("terminé !");
deferred.reject("terminé mais mal !");
});
return deferred.promise;
}
};
return Service;
});
app.controller('GreetController', function($scope, MaPromesse) {
MaPromesse.get().then(function(data) {
$scope.data = data;
}, function(reason) {
$scope.data = reason;
});
});
http://plnkr.co/edit/wo4DNu?p=preview
directives
, tous les éléments HTML du formulaire sont 'étendus' par Angular:form
, input
, button
,..required
, min="4"
, type="email"
,..directives
de validation:ng-maxlength="20"
, ng-pattern="/^[a-zA-Z]+$/"
,..directives
de validation, y compris asynchrones!app.controller('MainController', function($scope) {
$scope.newUser = {
firstname: '',
lastname: '',
email: '',
website: ''
};
$scope.createUser = function(user) {
alert('nouveau user valide prêt!');
};
});
http://plnkr.co/edit/0oFGvN?p=preview
Le formulaire et chaque champ ont des booléens assignés:
$dirty
: Champ ou formulaire déjà modifié.$pristine
: Champ ou formulaire pas encore modifié.$valid
: Champ ou formulaire valide.$invalid
: Champ ou formulaire invalide.$error
: Erreur courante du champ ou formulaire.$scope.registerForm.$valid; /* true si formulaire valide */
$scope.registerForm.nom.$invalid; /* true si champ nom n'est pas valide */
$scope.registerForm.courriel.$dirty; /* true si champ courriel n'a pas encore été 'utilisé' */
$scope.registerForm.prenom.$error.required; /* true si validation required du champ prenom non remplie */
$scope.registerForm.courriel.$error.email; /* true si validation email du champ courriel non remplie */
http://plnkr.co/edit/Wix7sD?p=preview
Encore mieux, le module ngMessages
var app = angular.module('myApp', ['ngMessages']);
http://plnkr.co/edit/00vSyb?p=preview
Angular ajoute des classes CSS sur les input
en fonction de leur état.
/* SASS */
form input {
border-width: 4px;
&.ng-dirty {
&.ng-valid {
border-color: green;
}
&.ng-invalid {
border-color: red;
}
}
}
ngRoute
: Basé sur les routes (#/posts/12), trés basique, pas de vues imbriquées, plus mis à jour.uiRouter
: Basé sur les states ('app.posts.details'), librairie remplaçante, beaucoup plus souple.ngNewRouter
Nouveau router qui arrive avec la version 1.4 et sera aussi utilisé dans Angular 2.app.config(function($routeProvider) {
$routeProvider.when(
'/page1',
{
templateUrl: 'page1.tpl.html'
}
);
$routeProvider.when(
'/page2/:name',
{
controller: 'Page2Ctrl',
templateUrl: 'page2.tpl.html'
}
);
$routeProvider.when(
'/pages3/article/:id',
{
controller: function($scope, $routeParams) {
$scope.idArticle = $routeParams.id;
},
template: '\
PAGE3
\
Details de l\'article id: {{ idArticle }}
\
'
}
);
$routeProvider.otherwise({redirectTo: '/page1'});
});
http://plnkr.co/edit/rFUxCD?p=preview
app.config(function($stateProvider, $urlRouterProvider) {
$stateProvider.state(
'app',
{
url: '/',
template: ''
}
);
$stateProvider.state(
'app.page1',
{
url: 'page1/',
templateUrl: 'page1.tpl.html'
}
);
$stateProvider.state(
'app.page1.detailA',
{
url: 'detailA',
template: '\
DETAILA
\
'
}
);
$stateProvider.state(
'app.page1.detailB',
{
url: 'detailB',
template: '\
DETAILB
\
'
}
);
$stateProvider.state(
'app.page2',
{
url: 'page2/:name',
controller: 'Page2Ctrl',
templateUrl: 'page2.tpl.html'
}
);
$stateProvider.state(
'app.page3',
{
url: 'page3/article/:id',
controller: function($scope, $stateParams) {
$scope.idArticle = $stateParams.id;
},
template: '\
PAGE3
\
Details de l\'article id: {{ idArticle }}
\
'
}
);
$urlRouterProvider.otherwise('/page1');
});
http://plnkr.co/edit/xSpKKD?p=preview
ngAnimate
.class
.ngAnimate
ajoute de classes CSS sur diférentes directives:
var app = angular.module('app', ['ngAnimate']);
Hi guys !!!
/* SASS */
.myTitle.ng-hide-remove {
@include animation(0, 1s, rollIn); // zoomInDown bounceInLeft
}
.myTitle.ng-hide-add {
@include animation(0, 1s, rollOut); // zoomOutDown bounceOutRight
}
http://plnkr.co/edit/IbgIcn?p=preview
Hi guys !!!
app.animation('.myTitle', function () {
return {
enter: function(element, done) {
element.css({
opacity: 0,
position: "relative",
top: '-50px'
})
.animate({
opacity: 1,
top: '0'
}, 500, done);
},
leave: function(element, done) {
element.css({
opacity: 1,
position: "relative"
})
.animate({
opacity: 0,
right: '100%'
}, 1000, done);
}
};
});
http://plnkr.co/edit/dN3fpG?p=preview
Tout est testable:
Controllers
, Services
, Directives
, Filters
,...
ainsi que l'application dans sa globalité!
L'application est composé de modules, eux-mêmes composés de controllers
, services
, directives
, filters
,..
Toutes ces briques communiquent entre elles par le biais d'une API publique et se comportent comme des "boites noires".
Les tests unitaires vérifient ces API publiques.
Les outils:
Karma
: Application NodeJS pour faire tourner les tests.Jasmine
: Framework pour écrire les tests (ou Mocha
).ngMock
: Utilitaires et services spécial tests unitaires.$httpBackend
: Service pour simuler $http
.Possibilité de faire du TDD.
Test d'un controller
:
angular.module('monApp').controller('DashboardCtrl', function ($scope) {
$scope.score = 456585;
});
describe('Tests du Controller: DashboardCtrl', function () {
var DashboardCtrl,
scope;
beforeEach(module('monApp'));
beforeEach(inject(function ($controller, $rootScope) {
scope = $rootScope.$new();
DashboardCtrl = $controller('DashboardCtrl', {
$scope : scope
});
}));
it('le score doit etre defini!', function () {
expect(scope.score).toBeDefined();
expect(scope.score).toBe(456585);
});
});
.e2e.js
.Selenium
, webDriver
, Jasmine
..PhantomJS
(headless browser).// todo-spec.js
describe('angularjs homepage todo list', function() {
it('should add a todo', function() {
browser.get('http://www.angularjs.org');
element(by.model('todoText')).sendKeys('write a protractor test');
element(by.css('[value="add"]')).click();
var todoList = element.all(by.repeater('todo in todos'));
expect(todoList.count()).toEqual(3);
expect(todoList.get(2).getText()).toEqual('write a protractor test');
});
});
// conf.js
exports.config = {
seleniumAddress: 'http://localhost:4444/wd/hub',
specs: ['todo-spec.js']
};
// ligne de commande
protractor conf.js
Chrome s'ouvre, interactions, puis se ferme rapidement.
1 test, 2 assertions, 0 failures