Opinionated Angular styleguide for teams.
A standardized approach for developing Angular applications at triplelift. This styleguide touches on concepts, syntax and conventions.
Rules are in bold at the top of each bullet point, with example code shortly thereafter, and the "why" follows after that. The idea is that the rule is the most important - a standard approach has merits in its own right - and should be the most accessible. Code clarifying or even solidifying the rule comes next. If interested, explanations of the rule are provided after that, with the most significant explanation attached to the bullet point prefixed with "Most importantly".
Most of the content and examples in this guide are based off of John Papa's and Todd Motto's style guides. Check theirs out to compare against originals.
- Modules
- Controllers
- Services
- Directives
- Routing with promises
- Performance
- Angular wrapper references
- Comment standards
- Minification and annotation
-
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.
-
controllerAs syntax: Use the
controllerAssyntax over the classic "controller with $scope" syntax.<!-- avoid --> <div ng-controller="MainCtrl"> {{ someObject }} </div> <!-- recommended --> <div ng-controller="MainCtrl as vm"> {{ vm.someObject }} </div>
/* avoid */ function CustomerController($scope) { $scope.name = {}; $scope.sendMessage = function() { }; }
/* recommended - but see next section */ function CustomerController() { this.name = {}; this.sendMessage = function() { }; }
Why?
- Controllers are constructed, "newed" up, providing a single new instance. The
controllerAssyntax more closely resembles "JavaScript constructor" than "classic$scope" syntax. - Even with
controllerAs, the controller object is still bound to$scope, so you can still bind to the View...this fact ultimately leads some to even see thecontrollerAssyntax as high level syntactic sugar. - It promotes the use of binding to a "dotted" object in the View (e.g.
customer.nameinstead ofname), which is more contextual and easier to read. - Since the desired view binding can reached simply by
vm.viewBindingeven if nested within a child$scope, it discourages the use of$parentin the View. - Discourages the use of
$scopemethods inside a controller when they are better placed inside a factory or avoided altogether. - It does not prevent the use of
$scopemethods, such as$emit,$broadcast,$onor$watch, which may be accessed by injecting$scopeif necessary. - Most importantly, the
controllerAssyntax prevents "$scope soup". There may be nothing worse in large scale angular development than running intong-click="doOnClick(data)"in the View, looking into the controller "corresponding" to such View and being unable to locate either thedoOnClickordatadefinitions. At this point, of course, you must scale the $scope hierarchy until the nearest declarations of each property are found... no fun indeed. Taking advantage of the basic rules of JavaScript prototypal inheritance, placing wonderfulvmin front of each variable, as inng-click="vm.doOnClick(vm.data)"ensures that the controller actually associated with the View contains the corresponding definitions. That is because when Javascript attempts to locatevm, it will find the associated controller (bound to$scope) since it exists and is bound to$scope- success in this regard is guaranteed. Next, JavaScript will attempt to locatedoOnClickanddataon such controller and...vuala... either the corresponding definitions will be found bound to such controller orundefinedwill result.
- Controllers are constructed, "newed" up, providing a single new instance. The
-
controllerAs 'vm': Capture the
thiscontext of the Controller usingvm(short for "view model")/* avoid */ function CustomerController() { this.name = {}; this.sendMessage = function() { // wrong! ... this.name ... }; }
/* recommended */ function CustomerController() { var vm = this; vm.name = {}; vm.sendMessage = function() { // right! ... vm.name ... }; }
Why?
- Most importantly, the
thiskeyword is made available inside every Javascript function in order to gain reference to the invocation context if invoked as a method (e.g.someInvocationContext.someMethod()) and the new object created if invoked with thenewoperator as a constructor (e.g.someNewObject = new SomeConstructor()). Since angular internally invokes all functions registered as controllers with thenewoperator, anythisreference inside the controller function necessarily refers to the new object constructed, unless... (a big unless!) further nested inside another function inside the controller. Capturing the top levelthisreference of a controller once and at the top of the controller, instead of repeatingthiscalls throughout, encourages consistency and prevents any nestedthisreferences from incorrectly referencing some object other the object contstructed by the controller and available in the View.
- Most importantly, the
-
Bindable members up top: Place bindable members at the top of the controller, perferably alphabetized, instead of spreading throughout the controller code.
/* avoid */ function SessionsController() { var vm = this; vm.gotoSession = function() { /* ... */ }; vm.refresh = function() { /* ... */ }; vm.search = function() { /* ... */ }; vm.sessions = []; vm.title = 'Sessions'; }
/* recommended */ function SessionsController(sessionDataService) { var vm = this; vm.gotoSession = gotoSession; vm.refresh = refresh; vm.search = search; vm.sessions = []; vm.title = 'Sessions'; vm.write = sessionDataService.write; // 1 liner is OK //////////// function gotoSession() { /* */ } function refresh() { /* */ } function search() { /* */ } }
Why?
- Placing bindable members in one place is easier to read and easier to find.
- Placing bindable members at the top helps you instantly identify which members are "publicly" available in the View.
- Defining the functions below the bindable members moves the implementation details (& complexity) down.
- In Javascript, function declaration are hoisted so there are no concerns over using a function before it is defined (as there would be with function expressions), even if one function references another.
-
Presentational logic only (MVVM): Only place presentational logic in controllers; delegate any business logic 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); }); }; }
Note:
- The
$httpservice and the URL paths are not referenced directly in the controller. - Logic in controllers is used only to bind the appropriate data to the controller object.
Why?
- Logic may be reused by multiple controllers when placed within a service.
- Logic in a service can more easily be isolated in a unit test and mocked (when testing the controller).
- Simplifies and hides implementation details from the controller.
- The
-
Do not reuse controllers: One controller per view; do not resuse controllers.
Why?
- Reusing controllers with several views is brittle and requires good end-to-end (e2e) test coverage to ensure stability across large applications.
- Reusable logic can be moved into services should be moved into services (for all the reasons described in the previous rule), leaving controllers simple and focused.
-
Assign controllers in route definitions instead of templates
/* avoid */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html' }); }
<!-- avengers.html --> <div ng-controller="AvengersController as vm"> </div>
/* recommended */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'Avengers', controllerAs: 'vm' }); }
<!-- avengers.html --> <div> </div>
Why?
- Pairing a controller with a template through
$routeProviderconfigurations allows for template reuse. In other words, whenng-controlleris used inline, that's it... a pairing is made between template and controller, which prevents template reuse with another controller. - Most importantly, having all controller-template pairings upfront creates a sort of index for your application, which makes it easier to see where everything is, what goes with what and how the data flows throughout your application (see Routing with promises below for more).
Note:
- Although now even easier to do with the introduction of route definitions, controller reuse is still ill-advised for the reasons above.
- Pairing a controller with a template through
-
Make use of 'vm' when
$watching in a controller:$watch'es in a controller should make use of thecontrollerAssyntax.<input ng-model="vm.title"/>
function SomeController($scope, $log) { var vm = this; vm.title = 'Some Title'; $scope.$watch('vm.title', function(current, original) { $log.info('vm.title was %s', original); $log.info('vm.title is now %s', current); }); }
Note:
$watch'es are better avoided altogether to minimize$digetcycle load or placed in a Directive, if necessary.
-
Always use a factory: Use angular's
.factory()method over its.service()method.Service: acts as a
constructorfunction and is instantiated with thenewkeyword. Usethisfor public methods and variables// avoid angular .module('app') .service('logger', logger); function logger() { this.logError = function(msg) { /* */ }; }
Factory: is invoked like any ol' function and, accordingly, should return something!
// recommended angular .module('app') .factory('logger', logger); function logger() { return { logError: function(msg) { /* */ } }; }
Why?
- Most importantly, functions registered with the
.factory()and.service()methods are both services and all such Angular services are singletons (which, according to angular documentation, means that "each component dependent on a service gets a reference to the single instance generated by the service factory"). As a result,.service()and.factory()only differ in the way Objects are created, but not in what they can create. - Since they can perform the same tasks, it makes sense to only use one for the sake of consistency.
- Since it doesn't require use of the
thisparameter, the.factory()method is arguably the simpler of the two.
- Most importantly, functions registered with the
-
Follow the Revealing Module Pattern: Expose the callable members of the service (its interface) at the top.
/* avoid */ function dataService() { var someValue = ''; var save = function() { /* */ }; var validate = function() { /* */ }; return { save: save, someValue: someValue, validate: validate }; }
/* recommended */ function dataService() { var someValue = ''; return { save: save, someValue: someValue, validate: validate }; //////////// function save() { /* */ }; function validate() { /* */ }; }
Why?
- Placing accessible members at the top makes is easier to read.
- It helps you instantly identify which functions of the factory are publicly exposed and must be unit tested (or mocked).
- Defining the functions below the bindable members moves the implementation details (& complexity) down.
- Function declaration are hoisted so there are no concerns over using a function before it is defined (as there would be with function expressions), even if one function references another.
-
Any interaction between controllers and data should be mediated by a service: XHR calls, local storage, stashing in memory and other data operations belong in a service.
/* avoid */ // dataservice factory angular .module('app.core') .factory('dataservice', function($http, logger) { return { getAvengers: getAvengers }; function getAvengers() { return $http.get('/api/maa') .then(getAvengersComplete) .catch(getAvengersFailed); function getAvengersComplete(response) { return response.data.results; } function getAvengersFailed(error) { logger.error('XHR Failed for getAvengers.' + error.data); } } });
/* recommended */ // controller calling the dataservice factory angular .module('app.avengers') .controller('AvengersController', function(dataservice, logger) { var vm = this; vm.avengers = []; activate(); function activate() { return getAvengers().then(function() { logger.info('Activated Avengers View'); }); } function getAvengers() { return dataservice.getAvengers() .then(function(data) { vm.avengers = data; return vm.avengers; }); } });
Why?
- This makes it easier to test (mock or real) data calls when testing a controller.
- Data service implementations may have very specific code to commmunicate with the data repository. This may include headers, how to talk to the data, or other services such as $http. Encapsuating this logic into a data service hides the implementation from the outside consumers (perhaps a controller).
- Encapsulation/hiding implementation makes it easier to change.
- Most importantly, the controller's responsibility is for the presentation and gathering of information for the view. It should not care how it gets the data, just that it knows who to ask for it. Separating the data services cabins the logic related to how to get the data and lets the controller be simpler and more focused on the view.
-
Declaration restrictions: Only create element and attribute directives i.e.
{ restrict: 'EA' },{ restrict: 'A' }or{ restrict: 'E' }.<!-- avoid --> <!-- directive: my-directive --> <div class="my-directive"></div> <!-- recommended --> <my-directive></my-directive> <div my-directive></div>
Why?
- Comment and class name declarations are confusing.
- Comments do not play nicely with older versions of IE.
-
Directives are the only place for DOM manipulation: Favor CSS, animation services and custom angular directives to perform DOM manipuation. For example, if the directive simply hides and shows, use ngHide/ngShow. However, if other javascript DOM manipulation is required, do so in a directive.
// 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);
Why?
- DOM manipulation can be difficult to test, debug, and there are often better ways (e.g. CSS)
- Most importantly, while it's true that controllers are provided access to the parent-most
$element, generally speaking, the desired DOM manipulation relates to some child element in the controller's View . Directives, on the other hand, are provided access to theelementunderlying the desired DOM changes. Using suchelementavoids unnecessarily having to search (possibly, by a unique identifier) for the element and more appropriately associates the DOM manipulation logic with the subject of such logic: the element! - Separating such logic out of the controllers avoids further complicating controller logic while providing encapsulation for DOM related logic. After all, Controllers have their own reason for being that is unrelated to DOM manipulation.
-
Naming conventions: Provide a short, unique and descriptive directive prefix such as
acmeSalesCustomerInfo, declared in HTML asacme-sales-customer-info.Why?
- Directives enter a global namespace and must be verbosely named as a result.
- The short prefix identifies the directive's context and origin. For example, a prefix of cc- may indicate that the directive is part of a CodeCamper app while acme- may indicate a directive for the Acme company.
-
All templating directives should be considered "components", restricted to
'E'and isolated: Directives that havetemplateortemplateUrldefinitions must specify{ restrict: 'E' }and{ scope: {} }.<!-- avoid --> <div my-example></div>
/* avoid */ angular .module('app') .directive('myExample', function() { return { restrict: 'EA', // <-- note templateUrl: 'app/feature/example.directive.html', controllerAs: 'vm', controller: function() {} }; });
<!-- recommended --> <my-example max="vm.max"></my-example>
/* recommended */ angular .module('app') .directive('myExample', function() { return { restrict: 'E', // <-- note templateUrl: 'app/feature/example.directive.html', scope: {}, // <-- note bindToController: { max: '=' }, // <-- note controllerAs: 'vm', controller: function() {} }; });
Note:
- Direcives with
templateortemplateUrldefinitions will replace any child nodes in the original html with the specified template (unless transcluding).
Why?
- Attribute directives, like
ng-clickandng-show, frequently manipulate the DOM or provide other behavior peripheral to data flow and view management. Restricting templating directives to type'E'clearly distinguishes them from these. - Most importantly, the negative effects of "scope soup" (see Controllers for more) become even more pronounced in the case of directives because they can be placed anywhere in the DOM tree no matter the
$scopelineage that awaits. Consequently, it can be even more important to isolate directive scopes. - Together, these settings -
{ restrict: 'E' }and{ scope: {} }- constitute what's become known within the angular community as a "component". Possibly the most successful design pattern of Angular 1 development, components were made a part of Angular 2 in a big way. In addition, theangular.componentmethod was introduced in Angular 1.5, which is a shortcut (a.k.a syntactic sugar) for a.directivethat defaults to these settings. Using these settings as frequently as possible, and especially in place of stand-alone controllers, can bring an angular codebase closer to the tried and true styles of the angular community and will better position the application for a future upgrade to angular 2 if/when desired.
- Direcives with
-
Use bindToController and controllerAs: When binding properties manually in the controller or passing them into the directive through HTML attributes, use
bindToControllerand thecontrollerAssyntax.<my-example max="77"></my-example>
/* avoid */ angular .module('app') .directive('myExample', function() { return { restrict: 'E', templateUrl: 'app/feature/example.directive.html', scope: { max: '=' }, controller: function($scope) { $scope.min = 3; console.log('CTRL: $scope.min = %s', $scope.min); console.log('CTRL: $scope.max = %s', $scope.max); } }; });
<!-- example.directive.html --> <!-- avoid --> <div>hello world</div> <div>max={{max}}<input ng-model="max"/></div> <div>min={{min}}<input ng-model="min"/></div>
/* recommended */ angular .module('app') .directive('myExample', function() { return { restrict: 'E', templateUrl: 'app/feature/example.directive.html', scope: {}, bindToController: { max: '=' }, controllerAs: 'vm', controller: function() { var vm = this; vm.min = 3; console.log('CTRL: vm.min = %s', vm.min); console.log('CTRL: vm.max = %s', vm.max); } }; });
<!-- example.directive.html --> <!-- recommended --> <div>hello world</div> <div>max={{vm.max}}<input ng-model="vm.max"/></div> <div>min={{vm.min}}<input ng-model="vm.min"/></div>
Note:
bindToControllerautomatically binds properties (that would otherwise be bound directly to$scope) directly to directive's controller, which itself is bound to$scope.
Why?
- For all the reasons described in the Controllers section above.
-
Resolve all data on which a controller may depend in the
resolvemethod of$routeProvider./* avoid */ angular .module('app') .controller('AvengersController', function(movieService) { var vm = this; // unresolved vm.movies; // resolved asynchronously movieService.getMovies().then(function(response) { vm.movies = response.movies; }); });
/* recommended */ // route-config.js angular .module('app') .config(config); function config($routeProvider) { $routeProvider .when('/avengers', { templateUrl: 'avengers.html', controller: 'AvengersController', controllerAs: 'vm', resolve: { moviesPrepService: function(movieService) { return movieService.getMovies(); } } }); } // avengers.js angular .module('app') .controller('AvengersController', function(moviesPrepService) { var vm = this; vm.movies = moviesPrepService.movies; });
Note:
- Any promise returned by the route resolve must resolve successfully before the controller is executed. Rejecting the promise cancels the route.
- If interested in increasing spead of the View load (and do not need to potentially cancel the state), consider the controller
activatetechnique described below instead.
Why?
- You can conditionally cancel a route inside the route resolver before the controller is activated.
- Most importantly, A controller may depend on data retrieved asynchronously. Resolving this data prior to loading the controller simplifies the controller logic tremendously. It also places data dependencies all in one place in the "index" of the application, e.g.
app.js, which makes it easier to reason about data flow.
-
If you must include async logic in controllers, keep such start-up logic to an
activatefunction./* avoid */ function AvengersController(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); }
/* recommended */ function AvengersController(dataservice) { var vm = this; vm.avengers = []; vm.title = 'Avengers'; activate(); //////////// function activate() { return dataservice.getAvengers().then(function(data) { vm.avengers = data; return vm.avengers; }); } }
Why?:
- Placing start-up logic in a consistent place is easier test
- It also makes it easier to locate, preventing the spread of the activation logic throughout the controller.
- It's easy to reuse for a refresh of the controller.
- Placing this logic inside the controller instead of inside the route resolve gets the user to the View faster, makes animations easy on the
ng-vieworui-view, and enables snappier UX.
-
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 <h1>{{ vm.title }}</h1> // recommended <h1>{{ ::vm.title }}</h1>
Why? : Binding once removes the watcher from the scope's
$$watchersarray after theundefinedvariable becomes resolved, thus improving performance in each dirty-check -
Consider $scope.$digest: Use
$scope.$digestover$scope.$applywhere it makes sense. Only child scopes will update$scope.$digest();
Why? :
$scope.$applywill call$rootScope.$digest, which causes the entire application$$watchersto dirty-check again. Using$scope.$digestwill dirty check current and child scopes from the initiated$scope
-
$document and $window: Use
$documentand$windowat all times to aid testing and Angular references// avoid function dragUpload () { return { link: function ($scope, $element, $attrs) { document.addEventListener('click', function () { }); } }; } // recommended function dragUpload ($document) { return { link: function ($scope, $element, $attrs) { $document.addEventListener('click', function () { }); } }; }
-
$timeout and $interval: Use
$timeoutand$intervalover their native counterparts to keep Angular's two-way data binding up to date// avoid function dragUpload () { return { link: function ($scope, $element, $attrs) { setTimeout(function () { // }, 1000); } }; } // recommended function dragUpload ($timeout) { return { link: function ($scope, $element, $attrs) { $timeout(function () { // }, 1000); } }; }
-
jsDoc: Use jsDoc syntax to document function names, description, params and returns
/** * @name SomeService * @desc Main application Controller */ angular .module('app') .factory('SomeService', function() { return { doSomething: doSomething }; /** * @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} */ function doSomething(x, y) { return x * y; }; });
-
ng-annotate: Use ng-annotate for Gulp as
ng-minis 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
$injectannotation/** * @ngInject */ function MainCtrl (SomeService) { this.doSomething = SomeService.doSomething; } MainCtrl.$inject = ['SomeService']; angular .module('app') .controller('MainCtrl', MainCtrl);
For anything else, including API reference, check the Angular documentation.