Cyril Moreau
Ingénieur Front-end @dailymotion. Participant fréquent à @MarseilleJS
webteam - 14/06/2016
1 - Routes : basées sur des pages/views (w-href, "_params")
2 - Controlleurs : la "scope soup" infâme
3 - La logique dans le template html
4 - L'écriture du code
5 - Le manque de tests unitaires
6 - Commentaires de commit : "no message", "blabla", "prout"
1 - Routes : basées sur des pages/views (w-href, "_params")
- routes basées sur du "state" (faire ce qu'on fait dans .run, éviter $rootScope, $wStore..)
// Gestion des "resolve"
getSettings.$injector = ['$wModApi'];
function getSettings($wModApi) {
return $wModApi.settings.get();
}
// Gestion des routes
stateHelperProvider.state({
name: 'root',
template: '<ui-view/>',
abstract: true,
resolve: {
settings: getSettings
},
children: [{
name: 'EnterPhoneNumber',
url: '/EnterPhoneNumber',
templateUrl: 'app/views/EnterPhoneNumber/EnterPhoneNumber.html',
controller: 'EnterPhoneNumberCtrl',
controllerAs: 'ctrl'
}]
});
$urlRouterProvider
.when('EnterPhoneNumber', 'EnterPhoneNumber')
.otherwise('EnterPhoneNumber');2 - Controlleurs : la "scope soup" infâme
// Controlleur
(function () {
'use strict';
angular
.module('app')
.controller('CounterCtrl', CounterCtrl);
CounterCtrl.$inject = ['$location', '$routeParams', 'common', 'dataservice'];
function CounterCtrl($location, $routeParams, common, dataservice) {
var ctrl = this;
function increment() {
ctrl.count++;
}
function decrement() {
ctrl.count--;
}
ctrl.increment = increment;
ctrl.decrement = decrement;
}
}());2 - Controlleurs : la "scope soup" infâme
// Composant (remplaçant de directive qui prend un controlleur, oublier la fonction "link")
(function () {
'use strict';
angular
.module('app')
.component('counter', counter);
var counter = {
bindings: {
count: '=', // two-ways databinding
countOneWay: '<' // one way databinding (plus besoin des :: dans le template html)
},
controller: CounterCtrl
};
}());4 - L'écriture du code
- Tabs 4
- ESLint
- Attention aux boucles for in, for. = préférer prog fonctionnelle : utiliser Array.prototype.filter(), foreach, etc
var obj = {
firm: "Watchever",
team: "webteam"
};
for (var prop in obj) {
if( obj.hasOwnProperty(prop) ) {
console.log("obj." + prop + " = " + obj[prop]);
}
}
// obj.firm = Watchever
// obj.team = webteam4 - L'écriture du code
- Attention diff notation JSON != object :
// object
var mon_objet = {
firm: "Watchever",
team: webteam
};
// JSON notation
{
"firm": "Watchever",
"team": "webteam"
}parce que c'est bien
- supprimer variables de $scope inutilisées ou bindées inutilement (search dans le template)
-idem pour les services injectés au ctrl
pour faire simple : fonction init() et variables de $scope déclarées et commentées au début ou à la fin du fichier
=> avec angular 1.5 $onInit() et bindings{}
-transférer les gros bouts de code js dans des services, par unité de sens
injectable en phase de configuration
navigationProvider.jsonBreakpointsFilename = '/app/css/breakpoints.json';service > factory > provider
retourne un objet js, à grouper par thème
provider : doit avoir un méthode $get attachée à son this qui retourne l'objet
(la factory retourne l'objet directement, le service attache tout à this cf exemples)
injection des dépendances au niveau du $get !
(function () {
'use strict';
angular
.module('app')
.component('navigation', navigationProvider);
function navigationProvider() {
'use strict';
var that = this;
/* On déclare des données attachée sur le provider. PAS sur l objet que le provider va créer, attention. Sur le provider lui-même.*/
that.jsonBreakpointsFilename = null;
that.breakpoints = null;
// fonction anonyme qui retourne un objet (principe du provider)
that.$get = ['$http',
function($http) {
var navigation = {
setBreakpoints: function setBreakpoints() {
if (that.jsonBreakpointsFile !== null) {
$http.get(that.jsonBreakpointsFilename).success(function(data) {
that.breakpoints = data;
});
} else {
console.log("navigation: breakpoints.json file not initialised !");
}
},
getBreakpoints: function getBreakpoints(name) {
if (typeof that.breakpoints[name] !== "undefined") {
return parseInt(that.breakpoints[name].screen);
}
}
};
return navigation;
}];
}
}());(function () {
'use strict';
angular
.module('app')
.factory('profile', profile);
function profile() {
return {
name: "Anonymous",
id: null,
login: function login() {
// faire un truc pour se loger
},
logout: function logout() {
// faire un truc pour se deco
}
};
}
}());(function () {
'use strict';
angular
.module('app')
.service('profile', profile);
function profile() {
var that = this;
that.name = "Anonymous";
that.id = null;
that.login = function login() {
// faire un truc pour se loger
}
that.logout = function logout() {
// faire un truc pour se deco
}
}
}());ne plus utiliser les currentStep mais des promises
- $rootScope.loading qui conditionne l'affichage
puis dans les ctrl:
var getTracks = function(contentId) {
var deferred = $q.defer();
$wModApi.users_playlists.me.contents.get(contentId, fields, range, expand).then(function (data) {
[...]
deferred.resolve();
});
return deferred.promise;
};
var init = function(){
getTracks(contentId).then(function() {
[...]
$rootScope.loading = false;
};
};dans le cas de promises multiples, chaînage
var getTracks = function(contentId) {
var deferred = $q.defer();
$wModApi.users_playlists.me.contents.get(contentId, fields, range, expand).then(function (data) {
[...]
deferred.resolve();
});
return deferred.promise;
};
var getRelatedTitles = function (contentId) {
var deferred = $q.defer();
$wModApi.contents.similars.get(contentId, range, expand, fields).then(function (data) {
[...]
deferred.resolve();
});
return deferred.promise;
}
var init = function(){
var p1 = getTracks(contentId);
var p2 = getRelatedTitles(contentId);
$q.all([p1.then(...), p2.then(...)].then(function() {
$rootScope.loading = false;
});
};on centralise tout dans w_mod_seed (todo gulpfile unifié en passant)
var deferred = $q.defer();
deferred.notify();
deferred.resolve();
deferred.reject();
return deferred.promise;var promise = asyncFn();
promise.then(function() {
alert('Success');
}, function(reason) {
alert('Failed: ' + reason);
}, function(update) {
alert('Got notification: ' + update);
});$scope.$emit('event', element);$scope.$broadcast('event', element);$scope.$on('event', function($event) {
});$timeout(function(){
//delayed
},1000);$translate('PP_label_ItemNbr').then(function(data) {
$scope.label = data;
});$translate('PP_label_ItemNbr',{ v0: 10 }).then(function(data) {
$scope.label = data;
});{{'PP_label_ItemNbr' | translate}}<h3 translate="{{ 'PP_label_ItemNbr' }}" translate-values="{v0 : content.title}"></h3>! ajouter la clé dans hiddentranslate.html
ngDialog.open({
showClose: true,
preCloseCallback: function () {},
template: 'app/views/dialog/myDialog.html',
controller: ['$scope', function ($dialogScope) {
$dialogScope.type = 'remove';
$dialogScope.delete = function () {
};
}]
});install
- bower link dans le projet w_mod_tile
- bower link w-mod-tile dans le projet principal wmusic (bower unlink w-mod-tile)
maj
pull du composant
1 - faire ses modifs dans le composant (gulp watch qui tourne) ->commit->gulp publish (v2.2.7)
2- faire évoluer la version dans le bower.json du projet principal ->commit
"w-mod-tile": "git@git.std1.prod.adm:w_mod_tile.git#v2.2.7"
3-avertir les copains sur slack
maj
pull projet principal, bower update
6 - Commentaires de commit : "no message", "blabla", "prout"
ATTENTION : pas d'espaces entre ) et :
5 - Le manque de tests unitaires
On réalise les tests unitaire avec les outils suivants :
- Karma : L'outil CLI qui permet de tester des applications sur navigateurs
- Jasmine: Un framework javascript permettant de structurer les tests avec en plus des fonctions prêtes à l'emploi
- NgMock: Il permet d' injecter et de simuler les services d'AngularJS d'où l'intérêt d' éviter les variables globales (dans l'objet window) pour pouvoir les mocker facilement en faisant des services
Chaque test se trouve dans le dossier du code à tester
Tester un controller
(function () {
'use strict';
angular
.module('myApp.view3')
.controller('View3Ctrl', View3Ctrl);
View3Ctrl.$inject = ['$scope', 'view3Service'];
function View3Ctrl($scope, view3Service) {
var vm = this;
vm.name = view3Service.getViewName();
}
}());Le contenu est chargé via un service que nous allons devoir mocker
Tester un controller
'use strict';
describe('myApp.view3 module', function () {
var $controller, view3ServiceMock, $scope, controller;
beforeEach(function() {
view3ServiceMock = jasmine.createSpyObj('view3Service', ['getViewName']);
module('myApp.view3');
inject(function($rootScope, $controller) {
$scope = $rootScope.$new();
view3ServiceMock.getViewName.and.returnValue('My name');
controller = $controller('View3Ctrl', {
$scope: $scope,
view3Service: view3ServiceMock
});
});
});
it('should return the name', function (){
$scope.name = view3ServiceMock.getViewName();
expect(view3ServiceMock.getViewName).toHaveBeenCalled();
expect($scope.name).toEqual('My name'); // expect(controller.name).toEqual('My name');
});
});
Tester un service
(function () {
'use strict';
angular
.module('myApp.view3')
.service('view3Service', view3Service);
view3Service.$inject = ['$http', '$q'];
function View3Ctrl($http, $q) {
return {
url: 'path_to_url',
getViewName: function(){
q = $q.defer()
$http.jsonp(url)
.then(function(result){
q.resolve(result.data)
})
return q.promise;
}
};
}());Tester un service
describe('view3Service', function(){
var view3Service;
beforeEach(function(){
angular.mock.module('myApp.view3')
angular.mock.inject(function(_view3Service_){
view3Service= _view3Service_
})
})
it("Should have a getViewName", function(){
expect(view3Service.getViewName).to.be.a('function')
})
});Tester un service
describe('view3Service', function(){
var view3Service, $http, url = "http://url/api.fr";
beforeEach(function(){
angular.mock.module('myApp.view3')
angular.mock.inject(function(_view3Service_, $httpBackend){
view3Service= _view3Service_
$http = $httpBackend
})
})
afterEach(function(){
$http.verifyNoOutstandingExpectation();
$http.verifyNoOutstandingRequest();
})
it("Should have a getViewName method", function(){
expect(view3Service.getViewName).to.be.a('function')
})
it("Should call JSONP", function(){
$http.expectJSONP(view3Service.url).respond(false)
view3Service.getViewName(url)
$http.flush()
})
it("Should return count", function(){
count = 0;
$http.expectJSONP(view3Service.url).respond({count: 2})
view3Service.getViewName(url).then(function(c){
count = c
})
$http.flush()
expect(count).to.be.equal(2)
})
})Tester une directive
(function () {
'use strict';
angular.module('myApp.directives').directive('alerts', alerts);
function alerts() {
return {
restrict: 'E',
template: '<div><div class="alert" ng-repeat="message in messages">{{ message.msg }}</div></div>',
scope: {
messages: '=msgs'
},
link: function(scope, element, attrs){
scope.demo = 1
}
};
}());Tester une directive
describe.only('Alert directive', function(){
var scope? element;
beforeEach(function(){
angular.mock.module('myApp.directives')
angular.mock.inject(function($compile, $rootScope){
scope = $rootScope.$new()
element = $($compile('<alerts msgs="aa"></alerts>')(scope))
$('body').append(element)
scope.$digest()
})
});
afterEach(function(){
element.remove()
})
it('Should display alerts', function(){
scope.aa = [{type: 'success', msg: 'Bravo'}, {type: 'success', msg: 'Bravo'}]
scope.$digest()
expect($('.alert', element).length).to.be.equal(2)
expect($('.alert:first', element).text()).to.be.equal('Bravo')
})
it('Should have dmeo in the scope', function(){
expect(element.isolateScope().demo).to.be.equal(1)
})
it('should be visible', function(){
scope.aa = [{type: 'success', msg: 'Bravo'}]
scope.$digest()
expect($('.alert').is(':visible')).to.be.true
})
});Tester un provider
(function () {
'use strict';
sample.$inject = ['config', 'anotherProvider'];
angular.module('myApp.providers').provider('sample', sample);
function sample(config, anotherProvider) {
this.configureOptions = function(options){
if(options.allow){
anotherProvider.register(appConstants.ALLOW);
} else {
anotherProvider.register(appConstants.DENY);
}
};
this.$get = function(){};
}
}());Tester un provider
beforeEach(module("myApp.providers"));
beforeEach(function(){
module(function(anotherProvider, config, sampleProvider){
_anotherProvider_=anotherProvider;
_config_=config;
_sampleProvider_=sampleProvider;
});
});
beforeEach(inject());
it('should call register with allow', function(){
_sampleProvider_.configureOptions({allow:true});
expect(_anotherProvider_.register).toHaveBeenCalled();
expect(_anotherProvider_.register).toHaveBeenCalledWith(_config_.ALLOW);
});By Cyril Moreau
webteam Watchever