1. expression
dans HTML = watcher
ajouté au scope
.
{{ 'variable = ' + maVar }}
...
// code explicatif non fonctionnel !!
$scope.$watch(function() {
return $parse(" 'variable = ' + maVar ")($scope);
}, function(newValue, oldValue) {
// met à jour le contenu de l'élément du DOM ('element.text()') avec nouvelle valeur.
});
$scope.$watch(function() {
return $parse(" getScore() ")($scope);
}, function(newValue, oldValue) {
// met à jour le contenu de l'élément du DOM ('element.text()') avec nouvelle valeur.
});
$scope.$watch(function() {
return $parse(" {'text-success': awesome, 'text-large': giant } ")($scope);
}, function(newValue, oldValue) {
// met à jour les classes de l'élément du DOM ('element.addClass()' / 'element.removeClass()') avec nouvelle valeur.
});
$scope.$watch(function() {
return $parse(" isTermsOk ")($scope);
}, function(newValue, oldValue) {
// met à jour le DOM ('element.html()') en ajoutant ou supprimant les éléments enfants suivant si true/false.
});
2. Quand un "digest cycle" tourne, les expressions
sont parsées et si résultat diffère du précédent, la fonction associée est lancée (dirty checking).
// code explicatif non fonctionnel !!
var Scope = function() {
this.$$watchers = [];
this.$watch = function(watcherFn, listenerFn) {
this.$$watchers.push({
watcherFn: watcherFn,
listenerFn: listenerFn
});
};
this.$digest = function() {
this.$$watchers.foreach(function(watcher) {
var newValue = watcher.watcherFn();
var oldValue = watcher.last;
if(newValue !== oldValue) {
watcher.listenerFn(newValue, oldValue);
watcher.last = newValue;
}
});
};
}
Matthieu Lux - ngEurope
3. Un "digest cycle" n'est jamais lancé par $scope.$digest()
mais par $scope.$apply()
.
// code explicatif non fonctionnel !!
var Scope = function() {
//....
this.$apply = function(exprFn) {
try {
exprFn();
} finally {
this.$digest();
}
};
}
app.controller('MainCtrl', function($scope, $timeout) {
$scope.myVar = 'titi';
setTimeout(function() {
$scope.myVar = 'toto';
}, 2000);
// > la vue ne sera pas mise à jour..
// Angular n'est pas averti de la maj !
setTimeout(function() {
$scope.$apply(function() {
$scope.myVar = 'toto';
});
}, 4000);
// > la vue sera mise à jour !
$timeout(function() {
$scope.myVar = 'toto';
},6000);
// > la vue sera mise à jour !
});
Dans l'écosystéme Angular, le $apply
est appelé frequemment sans qu'on le voit:
ngClick
, ngDblclick
, ngMouseenter
, ...$timeout
, $interval
, $q
, $http
, ...$apply
.$apply
est principalement utilisé pour l'intégration de libraries externes à Angular.watchers
(10 fois max).Possibilité de faire des watchers
en JS:
app.controller('MainCtrl', function($scope) {
$scope.myVar = 'titi';
$scope.$watch(function() {
return $scope.myVar;
}, function(newValue, oldValue) {
// la variable myVar vient de changer !
});
var unbinder = $scope.$watch('myVar', function(newValue, oldValue) {
// la variable myVar vient de changer !
});
// plus tard, on supprime le watcher:
unbinder();
});
controllers
, filtres
, services
,..Dans la pratique, chaque provider posséde une méthode $get
qui retourne l'instance et peut comporter des propriétés ou méthodes servant à configurer l'instanciation du service associé.
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.
factory()
et service()
sont juste des manières plus simples d’écrire un provider()
.
$get
obligatoire retournant l'instance du service.var app = angular.module('myapp');
app.provider('MyService', function() {
var scale = 1;
this.setScale = function(newScale) {
scale = newScale;
};
this.$get = function ($window) {
return {
getScreenWidth: function() {
return $window.innerWidth * scale;
},
getScreenHeight: function() {
return $window.innerHeight * scale;
}
};
};
});
app.config(function(MyServiceProvider) {
MyServiceProvider.setScale(10);
});
app.controller('AppCtrl', function($scope, MyService) {
var _width = MyService.getScreenWidth();
});
provider()
.$get
précédente.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();
});
provider()
.new
pour créer l'instance du service.return
un objet, tout ce qui est attaché à this
sera accessible par le biais du service.app.service('MyService', function($window) {
var scale = 1;
this.getScreenWidth = function() {
return $window.innerWidth * scale;
};
this.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
).provider
lors de la phase de configuration du module.services
.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;
});
value()
mais est accessible lors de la phase de configuration du module.app.constant('UselessData', {
scale: 42
});
app.config(function(MyServiceProvider, UselessData) {
MyServiceProvider.setScale(UselessData.scale);
});
app.controller('AppCtrl', function($scope, UselessData) {
$scope.scale = UselessData.scale;
});
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();
};
});
En pratique,factory()
etvalue()
sont utilisés dans la plupart du cas.
$http
, $location
, $filter
, $window
, $timeout
, $interval
, $animate
, $rootScope
, $document
,..
Providers
associés pour les configurer au démarrage:$httpProvider
, $locationProvider
, $filterProvider
, $animateProvider
,..
"Chaque requête d’un client vers un serveur doit contenir toute l’information nécessaire pour permettre au serveur de comprendre la requête, sans avoir à dépendre d’un contexte conservé sur le serveur. Cela libère de nombreuses interactions entre le client et le serveur."
Ajout d'un cookie avec un ID de session sur le navigateur de l'utilisateur aprés l'authentificaion.
Le serveur lit ce cookie à chaque requête suivante pour identifier l'utilisateur.
Génération d'un "passwordDigest" coté client à partir du login + pwd + salt qui est ajouté à chaque requête REST.
Possibilité de faire nativement avec Symfony2
Requête d'authentification qui retourne un "JWT access token" à durée limité qui est ajouté à chaque requête REST.
Bundle Symfony2 LexikJWTAuthenticationBundle.
Solution de plus en plus utilisée et trés bien pour les clients mobiles.
watchers
(scroll infini,..) en découpant son application.watchers
sur les tableaux.ngRepeat
avec 'track by'.ngIf
/ ngSwitch
et ngShow
/ ngHide
.{{ ::title }}
$scope.$digest()
au lieu de $scope.$apply()
. [Avec précaution!]scope
courant et ses descendants plutôt que toute l'application.filters
.Sur la prod, passer debugInfoEnabled
à false
:
app.config(function($compileProvider) {
$compileProvider.debugInfoEnabled(false);
});
Plus sur ngmodules.org
Surcouches d'Angular pour applications mobiles hybrides:
Equivalent de Composer
pour les libraires Front.
bower init // démarrage d'un projet bower
bower install jquery --save // installation nouvelle librairie front
bower install jquery#2.15.5 --save // installation d'une version spécifique
bower uninstall restangular --save // desinstallation une librairie
bower list [--offline] [--paths] // listing des librairies installées
bower search bxslider // recherche d'une librairie
bower update angular --save // mise à jour d'une librairie déjà installé
bower info angular-google-maps // liste les versions disponibles
bower cache clean // suppression cache bower
Compass
est une librairie SASS comprenant de nombreux mixins.@mixin box-shadow($properties...) {
-moz-box-shadow: $properties;
-webkit-box-shadow: $properties;
-o-box-shadow: $properties;
box-shadow: $properties;
}
$color_start: #f5f3f4;
$color_hover_start: #F4F4F4;
@for $i from 0 through 15 {
$darken_value: min(($i + 1) * 6, 100);
$darken_value_more: min($darken_value + 8, 100);
$darken_value_more_more: min($darken_value + 20, 100);
&.level-#{$i} {
background: darken($color_start, $darken_value);
border: 1px solid darken($color_start, $darken_value_more);
@include box-shadow(inset 0px 0px 10px 0px darken($color_start, $darken_value_more_more));
&:hover {
border-color: #666666;
}
.options {
border-bottom: 1px solid darken($color_start, $darken_value_more);
}
}
}
.level-0 {
background: #e7e2e5;
border: 1px solid #d5ccd0;
-moz-box-shadow: inset 0px 0px 10px 0px #b9abb2;
-webkit-box-shadow: inset 0px 0px 10px 0px #b9abb2;
-o-box-shadow: inset 0px 0px 10px 0px #b9abb2;
box-shadow: inset 0px 0px 10px 0px #b9abb2; }
.level-0:hover {
border-color: #666666; }
.level-0 .options {
border-bottom: 1px solid #d5ccd0; }
.level-1 {
background: #d9d2d5;
border: 1px solid #c7bbc1;
-moz-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
-webkit-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
-o-box-shadow: inset 0px 0px 10px 0px #ab9aa2;
box-shadow: inset 0px 0px 10px 0px #ab9aa2; }
.level-1:hover {
border-color: #666666; }
.level-1 .options {
border-bottom: 1px solid #c7bbc1; }
.level-2 {
background: #cbc1c6;
border: 1px solid #b9abb2;
-moz-box-shadow: inset 0px 0px 10px 0px #9d8993;
-webkit-box-shadow: inset 0px 0px 10px 0px #9d8993;
-o-box-shadow: inset 0px 0px 10px 0px #9d8993;
box-shadow: inset 0px 0px 10px 0px #9d8993; }
.level-2:hover {
border-color: #666666; }
.level-2 .options {
border-bottom: 1px solid #b9abb2; }
...
Gruntfile.js
Concaténer des fichiers en un seul (JS,..).
Copier des fichiers (fonts, templates,..).
Réécrie toutes les injections de dépendance pour la minification.
app.controller('MainCtrl', function($scope, MyCustomService) {
/* code du controlleur */
});
app.controller('MainCtrl', ['$scope', 'MyCustomService', function($scope, MyCustomService) {
/* code du controlleur */
}]);
function MainCtrl($scope, MyCustomService) {
/* code du controlleur */
}
MainCtrl.$inject = ['$scope', 'MyCustomService'];
app.controller('MainCtrl', MainCtrl);
Uglify un/des fichiers JS.
Compile ses fichiers SASS en CSS.
Minifie un/des CSS.
Minifie les templates HTML angular et les insert directement dans $templateCache
.
Evite de nombreuses requêtes ajax.
app.run(["$templateCache", function($templateCache) {
$templateCache.put("src/app/templates/page1.tpl.html",
"Page1: {{ myVar }}
"
);
$templateCache.put("src/app/templates/page2.tpl.html",
"Page2: {{ myVar }}
"
);
}]);
Tourne en tâche de fond et exécute d'autres tâches quand certains fichiers sont modifiés.
Exemples d'utilisation classique:
A vous de créer vos propres tâches qui sont des enchainements des autres tâches.
controller
, directive
, filter
,..).app.controller('app.library.PhotosListCtrl', function(...) {});
angular.bootstrap()
au lieu de ng-app
pour faire un beau loading.