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);
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)
$scope.$on('player.events.playVideo', function(event, video) {
$scope.playingVideo = video;
});
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