The Turn AngularJS styleguide (based off this).
- Folder structure
- Modules
- Controllers
- Services
- Directives
- Filters
- Routing resolves
- Publish and subscribe events
- Angular wrapper references
- Comment standards
- Minification and annotation
app/
|_module1/
|_configs/
|_module1RoutesConfig.js
|_constants/
|_module1Constant.js
|_controllers/
|_module1BarController.js
|_module1FooController.js
|_directives/
|_module1Foo.js
|_factories/
|_module1FooFactory.js
|_runs/
|_module1Baz.js
|_app.js
|_module2/
|_...
|_app.js
|_...
|_app.js
- Avoid empty folders
- All folder and file names should be in camelCase
- Put all angular blocks (controllers, directives, services, etc.) in their own files and
require()them from their module's app.js - Prefix all files with the module name to avoid name collisions when multiple modules are running within the same app
- Suffix config, controller, factory, provider, run, and service file names with the type of angular construct (eg.
Factory,Service, etc.) - Pieces of code that are shared between modules should live in their own repositories, and be installed and managed with bower. If a piece of code is too small or too specific to be separated, it should live in a shared/ folder (a sibling of your module folders)
- Each module should have an app.js, which is its entry point.
-
Definitions: Declare modules without a variable using the setter and getter syntax
// bad var app = angular.module('app', []); app.controller(); app.factory(); // good 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. -
When creating modules for open source use, be sure to prefix the module name with "turn/"
// bad
angular
.module('foo', [])
...
// good
angular
.module('turn/foo', [])
...-
Methods: Pass requires into module methods rather than assign as an inline callback
// bad angular .module('app', []) .controller('MainCtrl', function () { ... }) .service('SomeService', function () { ... }); // good angular .module('app', []) .controller('MainCtrl', require('./controllers/foo')) .service('SomeService', require('./services/foo'));
-
This aids with readability and reduces the volume of code "wrapped" inside the Angular framework
- Methods: When assigning methods or propeties to the scope, prefer
extendsyntax over direct assignment:
// bad
$scope.foo = 1;
$scope.bar = 2;
$scope.baz = function () { ... };
// good
angular.extend($scope, {
foo: 1,
bar: 2,
baz: function () { ... }
})-
controllerAs syntax: Controllers are classes, so use the
controllerAssyntax at all times<!-- bad --> <div ng-controller="MainCtrl"> {{ someObject }} </div> <!-- good --> <div ng-controller="MainCtrl as main"> {{ main.someObject }} </div>
-
In the DOM we get a variable per controller, which aids nested controller methods avoiding any
$parentcalls -
The
controllerAssyntax usesthisinside controllers which gets bound to$scope// bad function MainCtrl ($scope) { $scope.someObject = {}; $scope.doSomething = function () { }; } // good function MainCtrl () { this.someObject = {}; this.doSomething = function () { }; }
-
Only use
$scopeincontrollerAswhen necessary, for example publishing and subscribing events using$emit,$broadcast,$onor using$watch, try to limit the use of these, however and treat$scopeuses as special
-
Prefer services over factories and providers.
function SomeService () { this.someMethod = function () { }; }
-
Declaration restrictions: Only use
custom elementandcustom attributemethods for declaring your Directives (restrict: 'EA') depending on the Directive's role<!-- bad --> <div class="my-directive"></div> <!-- good --> <my-directive></my-directive>
-
Comment and class name declarations are confusing and should be avoided.
-
Templating: Prefer external templates over inlined HTML. Template names should be the dash-cased equivalents of their directive names.
// bad function someDirective () { return { template: '<div class="some-directive">' + '<h1>My directive</h1>' + '</div>' }; } // good function someDirective () { return { templateUrl: './templates/some-directive.html' }; }
-
DOM manipulation: Only takes place inside Directives, never a controller/service
// bad function UploadCtrl () { $('.dragzone').on('dragend', function () { // handle drop functionality }); } angular .module('app') .controller('UploadCtrl', UploadCtrl); // good 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.// bad // <div ng-upload></div> function ngUpload () { return {}; } angular .module('app') .directive('ngUpload', ngUpload); // good // <div drag-upload></div> function dragUpload () { return {}; } angular .module('app') .directive('dragUpload', dragUpload);
-
controllerAs: Use the
controllerAssyntax inside Directives also (angular 1.2+ only)// bad function dragUpload () { return { controller: function ($scope) { } }; } angular .module('app') .directive('dragUpload', dragUpload); // good function dragUpload () { return { controllerAs: 'dragUpload', controller: function () { } }; } angular .module('app') .directive('dragUpload', dragUpload);
-
Global filters: Create global filters only using
angular.filter()never use local filters inside Controllers/Services// bad function SomeCtrl () { this.startsWithLetterA = function (items) { return items.filter(function (item) { return /$a/i.test(item.name); }); }; } angular .module('app') .controller('SomeCtrl', SomeCtrl); // good 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
-
Promises: Resolve dependencies of a Controller in the
$routeProvidernot the Controller itself// bad 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); // good 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
resolveproperty for each Controller to couple the logic// bad function MainCtrl (SomeService) { this.something = SomeService.something; } function config ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controllerAs: 'main', controller: 'MainCtrl' resolve: { doSomething: function () { return SomeService.doSomething(); } } }); } // good function MainCtrl (SomeService) { this.something = SomeService.something; } MainCtrl.resolve = { doSomething: function () { return SomeService.doSomething(); } }; function config ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controllerAs: 'main', controller: 'MainCtrl' resolve: MainCtrl.resolve }); }
-
This keeps resolve dependencies inside the same file as the Controller and the router free from logic
-
$scope: event emitters should be avoided when possible (prefer
$watches and 2-way binding instead). When necessary, use the$emitand$broadcastmethods to trigger events to direct relationship scopes only.// up the $scope $scope.$emit('customEvent', data); // down the $scope $scope.$broadcast('customEvent', data);
-
$rootScope: use only
$emitas an application-wide event bus and remember to unbind listeners// all $rootScope.$on listeners $rootScope.$emit('customEvent', data);
-
Hint:
$rootScope.$onlisteners are different than$scope.$onlisteners and will always persist so they need destroying when the relevant$scopefires the$destroyevent// call the closure var unbind = $rootScope.$on('customEvent'[, callback]); $scope.$on('$destroy', unbind);
-
$document and $window: Use
$documentand$windowat all times to aid testing and Angular references// bad function dragUpload () { return { link: function (scope, element, attrs) { document.addEventListener('click', function () { }); } }; } // good function dragUpload () { return { link: function (scope, element, attrs, $document) { $document.addEventListener('click', function () { }); } }; }
-
$timeout and $interval: Use
$timeoutand$interval(angular 1.2+ only) over their native counterparts to automatically trigger digests when the timer fires// bad function dragUpload () { return { link: function (scope, element, attrs) { setTimeout(function () { scope.$apply(function(){ ... }); }, 1000); } }; } // good function dragUpload () { return { link: function (scope, element, attrs, $timeout) { $timeout(function () { ... }, 1000); } }; }
-
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);
-
ng-annotate: Use ng-annotate and comment functions that need automated dependency injection using
/* @ngInject */// controller.js module.exports = /* @ngInject */ function (SomeService) { this.doSomething = SomeService.doSomething; } // app.js angular .module('app') .controller('MainCtrl', require('./controllers/controller'));
-
Which ng-annotate compiles to the following annotated code
// controller.js module.exports = /* @ngInject */ ['SomeService', function (SomeService) { this.doSomething = SomeService.doSomething; }]; // app.js angular .module('app') .controller('MainCtrl', require('./controllers/controller'));
For anything else, API reference, check the Angular documentation.
Open an issue first to discuss potential changes/additions.
Copyright (c) 2014 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.