GitHub - toddmotto/angularjs-styleguide at angular-old-es5 (original) (raw)
Angular 1.4.x styleguide (ES5/old version)
For the new ES2015, component architecture guide please see
Opinionated Angular styleguide for teams by @toddmotto
A standardised approach for developing Angular applications in teams. This styleguide touches on concepts, syntax, conventions and is based on my experience writing, talking, and building Angular applications.
Join the Ultimate AngularJS experience and fully master basic and advanced Angular features
Community
John Papa and I have discussed in-depth styling patterns for Angular and as such have both released separate styleguides. Thanks to those discussions, I've learned some great tips from John that have helped shape this guide. We've both created our own take on a styleguide. I urge you to check his out to compare thoughts.
See the original article that sparked this off
Table of Contents
- Modules
- Controllers
- Services and Factory
- Directives
- Filters
- Routing resolves
- Publish and subscribe events
- Performance
- Angular wrapper references
- Comment standards
- Minification and annotation
Modules
Definitions: Declare modules without a variable using the setter and getter syntax
// avoid
var app = angular.module('app', []);
app.controller();
app.factory();
// recommended
angular
.module('app', [])
.controller()
.factory();Note: Using
angular.module('app', []);
sets a module, whereasangular.module('app');
gets the module. Only set once and get for all other instances.Methods: Pass functions into module methods rather than assign as a callback
// avoid
angular
.module('app', [])
.controller('MainCtrl', function MainCtrl () {
})
.service('SomeService', function SomeService () {
});
// recommended
function MainCtrl () {
}
function SomeService () {
}
angular
.module('app', [])
.controller('MainCtrl', MainCtrl)
.service('SomeService', SomeService);ES6 Classes are not hoisted, which will break your code if you rely on hoisting
This aids with readability and reduces the volume of code "wrapped" inside the Angular framework
IIFE scoping: To avoid polluting the global scope with our function declarations that get passed into Angular, ensure build tasks wrap the concatenated files inside an IIFE
(function () {
angular
.module('app', []);// MainCtrl.js
function MainCtrl () {
}angular
.module('app')
.controller('MainCtrl', MainCtrl);// SomeService.js
function SomeService () {
}angular
.module('app')
.service('SomeService', SomeService);// ...
})();
Controllers
controllerAs syntax: Controllers are classes, so use the
controllerAs
syntax at all times{{ someObject }}{{ vm.someObject }}In the DOM we get a variable per controller, which aids nested controller methods, avoiding any
$parent
callsThe
controllerAs
syntax usesthis
inside controllers, which gets bound to$scope
// avoid
function MainCtrl ($scope) {
$scope.someObject = {};
$scope.doSomething = function () {
};
}
// recommended
function MainCtrl () {
this.someObject = {};
this.doSomething = function () {
};
}Only use
$scope
incontrollerAs
when necessary; for example, publishing and subscribing events using$emit
,$broadcast
,$on
or$watch
. Try to limit the use of these, however, and treat$scope
as a special use caseInheritance: Use prototypal inheritance when extending controller classes
function BaseCtrl () {
this.doSomething = function () {
};
}
BaseCtrl.prototype.someObject = {};
BaseCtrl.prototype.sharedSomething = function () {
};
AnotherCtrl.prototype = Object.create(BaseCtrl.prototype);
function AnotherCtrl () {
this.anotherSomething = function () {
};
}Use
Object.create
with a polyfill for browser supportcontrollerAs 'vm': Capture the
this
context of the Controller usingvm
, standing forViewModel
// avoid
function MainCtrl () {
var doSomething = function () {
};
this.doSomething = doSomething;
}
// recommended
function MainCtrl () {
var vm = this;
var doSomething = function () {};
vm.doSomething = doSomething;
}
Why? : Function context changes the this
value, use it to avoid .bind()
calls and scoping issues
ES6: Avoid
var vm = this;
when using ES6
// avoid
function MainCtrl () {
let vm = this;
let doSomething = arg => {
console.log(vm);
};// exports
vm.doSomething = doSomething;
}
// recommended
function MainCtrl () {
let doSomething = arg => {
console.log(this);
};
// exports
this.doSomething = doSomething;
}
Why? : Use ES6 arrow functions when necessary to access the this
value lexically
Presentational logic only (MVVM): Presentational logic only inside a controller, avoid Business logic (delegate to Services)
// avoid
function MainCtrl () {var vm = this;
$http
.get('/users')
.success(function (response) {
vm.users = response;
});
vm.removeUser = function (user, index) {
$http
.delete('/user/' + user.id)
.then(function (response) {
vm.users.splice(index, 1);
});
};
}
// recommended
function MainCtrl (UserService) {
var vm = this;
UserService
.getUsers()
.then(function (response) {
vm.users = response;
});
vm.removeUser = function (user, index) {
UserService
.removeUser(user)
.then(function (response) {
vm.users.splice(index, 1);
});
};
}
Why? : Controllers should fetch Model data from Services, avoiding any Business logic. Controllers should act as a ViewModel and control the data flowing between the Model and the View presentational layer. Business logic in Controllers makes testing Services impossible.
Services and Factory
- All Angular Services are singletons, using
.service()
or.factory()
differs the way Objects are created.
Services: act as a constructor
function and are instantiated with the new
keyword. Use this
for public methods and variables
```javascript
function SomeService () {
this.someMethod = function () {
};
}
angular
.module('app')
.service('SomeService', SomeService);
```
Factory: Business logic or provider modules, return an Object or closure
- Always return a host Object instead of the revealing Module pattern due to the way Object references are bound and updated
function AnotherService () {
var AnotherService = {};
AnotherService.someValue = '';
AnotherService.someMethod = function () {
};
return AnotherService;
}
angular
.module('app')
.factory('AnotherService', AnotherService);
Why? : Primitive values cannot update alone using the revealing module pattern
Directives
- Declaration restrictions: Only use
custom element
andcustom attribute
methods for declaring your Directives ({ restrict: 'EA' }
) depending on the Directive's role - Comment and class name declarations are confusing and should be avoided. Comments do not play nicely with older versions of IE. Using an attribute is the safest method for browser coverage.
- Templating: Use
Array.join('')
for clean templating
// avoid
function someDirective () {
return {
template: '' +'
'My directive
' +
'
};
}
// recommended
function someDirective () {
return {
template: [
'','
'My directive
',
'
].join('')
};
}
Why? : Improves readability as code can be indented properly, it also avoids the+
operator which is less clean and can lead to errors if used incorrectly to split lines - DOM manipulation: Takes place only inside Directives, never a controller/service
// avoid
function UploadCtrl () {
$('.dragzone').on('dragend', function () {
// handle drop functionality
});
}
angular
.module('app')
.controller('UploadCtrl', UploadCtrl);
// recommended
function dragUpload () {
return {
restrict: 'EA',
link: function (scope, element, attrs) {
element.on('dragend', function () {
// handle drop functionality
});
}
};
}
angular
.module('app')
.directive('dragUpload', dragUpload); - Naming conventions: Never
ng-*
prefix custom directives, they might conflict future native directives
// avoid
//
function ngUpload () {
return {};
}
angular
.module('app')
.directive('ngUpload', ngUpload);
// recommended
//
function dragUpload () {
return {};
}
angular
.module('app')
.directive('dragUpload', dragUpload); - Directives and Filters are the only providers that have the first letter as lowercase; this is due to strict naming conventions in Directives. Angular hyphenates
camelCase
, sodragUpload
will become<div drag-upload></div>
when used on an element. - controllerAs: Use the
controllerAs
syntax inside Directives as well
// avoid
function dragUpload () {
return {
controller: function ($scope) {
}
};
}
angular
.module('app')
.directive('dragUpload', dragUpload);
// recommended
function dragUpload () {
return {
controllerAs: 'vm',
controller: function () {
}
};
}
angular
.module('app')
.directive('dragUpload', dragUpload);
Filters
- Global filters: Create global filters using
angular.filter()
only. Never use local filters inside Controllers/Services
// avoid
function SomeCtrl () {
this.startsWithLetterA = function (items) {
return items.filter(function (item) {
return /^a/i.test(item.name);
});
};
}
angular
.module('app')
.controller('SomeCtrl', SomeCtrl);
// recommended
function startsWithLetterA () {
return function (items) {
return items.filter(function (item) {
return /^a/i.test(item.name);
});
};
}
angular
.module('app')
.filter('startsWithLetterA', startsWithLetterA); - This enhances testing and reusability
Routing resolves
- Promises: Resolve Controller dependencies in the
$routeProvider
(or$stateProvider
forui-router
), not the Controller itself
// avoid
function MainCtrl (SomeService) {
var _this = this;
// unresolved
_this.something;
// resolved asynchronously
SomeService.doSomething().then(function (response) {
_this.something = response;
});
}
angular
.module('app')
.controller('MainCtrl', MainCtrl);
// recommended
function config ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
resolve: {
// resolve here
}
});
}
angular
.module('app')
.config(config); - Controller.resolve property: Never bind logic to the router itself. Reference a
resolve
property for each Controller to couple the logic
// avoid
function MainCtrl (SomeService) {
this.something = SomeService.something;
}
function config ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controllerAs: 'vm',
controller: 'MainCtrl'
resolve: {
doSomething: function () {
return SomeService.doSomething();
}
}
});
}
// recommended
function MainCtrl (SomeService) {
this.something = SomeService.something;
}
MainCtrl.resolve = {
doSomething: function (SomeService) {
return SomeService.doSomething();
}
};
function config ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controllerAs: 'vm',
controller: 'MainCtrl'
resolve: MainCtrl.resolve
});
} - This keeps resolve dependencies inside the same file as the Controller and the router free from logic
Publish and subscribe events
- $scope: Use the
$emit
and$broadcast
methods to trigger events to direct relationship scopes only
// up the $scope scope.scope.scope.emit('customEvent', data);
// down the $scope scope.scope.scope.broadcast('customEvent', data); - $rootScope: Use only
$emit
as an application-wide event bus and remember to unbind listeners
// all rootScope.rootScope.rootScope.on listeners rootScope.rootScope.rootScope.emit('customEvent', data); - Hint: Because the
$rootScope
is never destroyed,$rootScope.$on
listeners aren't either, unlike$scope.$on
listeners and will always persist, so they need destroying when the relevant$scope
fires the$destroy
event
// call the closure
var unbind = rootScope.rootScope.rootScope.on('customEvent'[, callback]); scope.scope.scope.on('$destroy', unbind); - For multiple
$rootScope
listeners, use an Object literal and loop each one on the$destroy
event to unbind all automatically
var unbind = [
rootScope.rootScope.rootScope.on('customEvent1'[, callback]),
rootScope.rootScope.rootScope.on('customEvent2'[, callback]),
rootScope.rootScope.rootScope.on('customEvent3'[, callback])
]; scope.scope.scope.on('$destroy', function () {
unbind.forEach(function (fn) {
fn();
});
});
Performance
- One-time binding syntax: In newer versions of Angular (v1.3.0-beta.10+), use the one-time binding syntax
{{ ::value }}
where it makes sense
// avoid{{ vm.title }}
// recommended{{ ::vm.title }}
_Why?_ : Binding once removes the watcher from the scope's `$$watchers` array after the `undefined` variable becomes resolved, thus improving performance in each dirty-check - Consider scope.scope.scope.digest: Use
$scope.$digest
over$scope.$apply
where it makes sense. Only child scopes will update
Why? :$scope.$apply
will call$rootScope.$digest
, which causes the entire application$$watchers
to dirty-check again. Using$scope.$digest
will dirty check current and child scopes from the initiated$scope
Angular wrapper references
- $document and window∗∗:Use‘window: Use
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mord mathnormal">in</span><span class="mord mathnormal">d</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.02691em;">w</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4653em;"></span><span class="mord">∗</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.10903em;">U</span><span class="mord mathnormal">se</span><span class="mord">‘</span></span></span></span>document
and$window
at all times to aid testing and Angular references
// avoid
function dragUpload () {
return {
link: function ($scope, element,element, element,attrs) {
document.addEventListener('click', function () {
});
}
};
}
// recommended
function dragUpload ($document) {
return {
link: function ($scope, element,element, element,attrs) {
$document.addEventListener('click', function () {
});
}
};
} - $timeout and interval∗∗:Use‘interval: Use
</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal">in</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.02778em;">er</span><span class="mord mathnormal" style="margin-right:0.03588em;">v</span><span class="mord mathnormal">a</span><span class="mord mathnormal" style="margin-right:0.01968em;">l</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">∗</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4653em;"></span><span class="mord">∗</span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="base"><span class="strut" style="height:0.6944em;"></span><span class="mord mathnormal" style="margin-right:0.10903em;">U</span><span class="mord mathnormal">se</span><span class="mord">‘</span></span></span></span>timeout
and$interval
over their native counterparts to keep Angular's two-way data binding up to date
// avoid
function dragUpload () {
return {
link: function ($scope, element,element, element,attrs) {
setTimeout(function () {
//
}, 1000);
}
};
}
// recommended
function dragUpload ($timeout) {
return {
link: function ($scope, element,element, element,attrs) {
$timeout(function () {
//
}, 1000);
}
};
}
Comment standards
- jsDoc: Use jsDoc syntax to document function names, description, params and returns
/** - @name SomeService
- @desc Main application Controller
/
function SomeService (SomeService) {
/*- @name doSomething
- @desc Does something awesome
- @param {Number} x - First number to do something with
- @param {Number} y - Second number to do something with
- @returns {Number}
*/
this.doSomething = function (x, y) {
return x * y;
};
}
angular
.module('app')
.service('SomeService', SomeService);
Minification and annotation
- ng-annotate: Use ng-annotate for Gulp as
ng-min
is deprecated, and comment functions that need automated dependency injection using/** @ngInject */
/** - @ngInject
*/
function MainCtrl (SomeService) {
this.doSomething = SomeService.doSomething;
}
angular
.module('app')
.controller('MainCtrl', MainCtrl); - Which produces the following output with the
$inject
annotation
/** - @ngInject
*/
function MainCtrl (SomeService) {
this.doSomething = SomeService.doSomething;
}
MainCtrl.$inject = ['SomeService'];
angular
.module('app')
.controller('MainCtrl', MainCtrl);
Angular docs
For anything else, including API reference, check the Angular documentation.
Contributing
Open an issue first to discuss potential changes/additions.
License
(The MIT License)
Copyright (c) 2015-2016 Todd Motto
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.