The principles we use to guide low-level decision making are:
import {dialogControllerModule} from './dialog.controller';
import template from './dialog.template.html!text';
_.extend could be replaced by Angular’s built in method angular.extend.$stateProvider could be replaced by $routeProvider.Note: For a complete setup see our Example ES6 Application Scaffold.
We organise our code as follows:
/app
/components
/alert
alert.directive.js
alert.directive.spec.js
alert.template.html
/config
main.config.js
/constants
api-url.constant.js
/routes
/customers
/index
customers-index.template.html
customers-index.route.js
customers-index.controller.js
customers-index.e2e.js
/helpers
/currency
currency-filter.js
currency-filter.spec.js
/unit
/e2e
/services
/creditors
creditors.js
creditors.spec.js
bootstrap.js
main.js
/assets
/fonts
/images
/stylesheets
404.html
index.html
Keep spec files in the same folder as the code being tested.
Components are encapsulated DOM components. Each component contains all the HTML, CSS, JavaScript, and other dependencies needed to render itself.
A view, made up of components and unique pieces of UI, that points to a URL. Like components, each route contains all the HTML, CSS, JavaScript, and other dependencies needed to render itself.
Services contain Business logic. For example, $http abstractions.
Configures Providers. For example, $locationProvider.html5Mode(true);.
Although JavaScript does not yet support constants, we run our application through Traceur, which supports const.
The constant should be named in all uppercase if it's a global constant that will be used across many different functions.
For example, export const API_URL = 'https://api.gocardless.com'.
If the constant is defined within a single function, it should be in regular camelCase.
Pure functions. For example, a currencyFilter might take in a number and format it for a certain currency. Helpers take input and return output without having any side effects.
Rules for using each of the core parts of AngularJS (routes, directives, controllers, modules, and templates).
Why: The page is rendered only when all data is available. This means views are only rendered once all the required data is available, and you avoid the user seeing any empty views whilst the data is loading.
// Recommended
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController',
controllerAs: 'ctrl',
resolve: {
customer: [
'Customers',
'$stateParams',
function customerResolver(Customers, $stateParams) {
return Customers.findOne({
params: { id: $stateParams.id }
});
}
]
}
});
// Avoid
// Note: Controller written inline for the example
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controllerAs: 'ctrl',
controller: [
'Customers',
'$stateParams',
function CustomersShowController(Customers, $stateParams) {
const ctrl = this;
Customers.findOne({
params: { id: $stateParams.id }
}).then(function(customers) {
ctrl.customers = customers;
});
}
]
});
offset and limit when paginating.Why: The current view should be accurately reflected in the URL, which means any page refresh puts the user back in the exact state they were in.
// Recommended
function nextPage() {
const currentOffset = parseInt($stateParams.offset, 10) || 0;
const limit = parseInt($stateParams.limit, 10) || 10;
const nextOffset = currentOffset + limit;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}
// Avoid
// Keeping route state in memory only
let currentOffset = 0;
const limit = 10;
function nextPage() {
const nextOffset = currentOffset + limit;
currentOffset = nextOffset;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}
a-z and at least one dash (-).Why: Custom elements must have a dash (namespace) to differentiate them from native elements and prevent future component collisions.
<!-- Recommended -->
<dialog-box></dialog-box>
<button click-toggle="isActive"></button>
<!-- Avoid -->
<dialog></dialog>
<button toggle="isActive"></button>
Why: Separates responsibility: element directives add content; attribute directives add behaviour; class attributes add style.
<!-- Recommended -->
<alert-box message="Error"></alert-box>
<!-- Replaced with: -->
<alert-box message="Error" class="ng-isolate-scope">
<div class="alert-box">
<span class="alert-box__message">Error</span>
</div>
</alert-box>
<!-- Avoid -->
<p alert-box message="Error"></p>
<!-- Replaced with: -->
<p alert-box message="Error">
<div class="alert-box">
<span class="alert-box__message">Error</span>
</div>
</p>
<!-- Recommended -->
<button prevent-default="click">Submit</button>
<!-- Avoid -->
<prevent-default event="click">Submit</prevent-default>
Why: Using an isolate scope forces you to expose an API by giving the component all the data it needs. This increases reusability and testability. When using a shared scope for attribute directives you should not write to it or rely on any existing data. Attribute directives should not have an isolate scope because doing so overwrites the current scope.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E'
};
}
]);
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A'
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A',
scope: {}
};
}
]);
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A',
scope: true
};
}
]);
bindToController.Why: It explicitly shows what variables are shared via the controller.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
scope: {}
};
}
]);
$scope.$on('$destroy', ...) to get rid of any event listeners or DOM nodes created outside the directive element.Why: It avoids memory leaks and duplicate event listeners being bound when the directive is re-created.
// Recommended
angular.module('adminExpandComponentModule', [])
.directive('adminExpand', [
'$window',
function adminExpand($window) {
return {
restrict: 'A',
scope: {},
link: function adminExpandLink(scope, element) {
function expand() {
element.addClass('is-expanded');
}
$window.document.addEventListener('click', expand);
scope.$on('$destroy', function onAdminExpandDestroy() {
$window.document.removeEventListener('click', expand);
});
}
};
}
]);
// Avoid
angular.module('adminExpandComponentModule', [])
.directive('adminExpand', [
'$window',
function adminExpand($window) {
return {
restrict: 'A',
scope: {},
link: function adminExpandLink(scope, element) {
function expand() {
element.addClass('is-expanded');
}
$window.document.addEventListener('click', expand);
}
};
}
]);
x-, polymer-, ng-.controllerAs syntax.Why: It explicitly shows what controller a variable belongs to, by writing {{ ctrl.foo }} instead of {{ foo }}.
// Recommended
$stateProvider.state('authRequired.customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController',
controllerAs: 'ctrl'
});
// Avoid
$stateProvider.state('authRequired.customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController'
});
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
scope: {}
};
}
]);
Why:
// Recommended
angular.module('customersShowControllerModule', [])
.controller('CustomersShowController', [
'customer', 'payments', 'mandates',
function CustomersShowController(customer, payments, mandates){
const ctrl = this;
_.extend(ctrl, {
customer: customer,
payments: payments,
mandates: mandates
});
}
]);
// Avoid
angular.module('customersShowControllerModule', [])
.controller('CustomersShowController', [
'Customers', 'Payments', 'Mandates', '$stateParams',
function CustomersShowController(Customers, Payments, Mandates, $stateParams){
const ctrl = this;
Customers.findOne({
params: { id: $stateParams.id }
}).then(function(customers) {
ctrl.customers = customers;
});
Payments.findAll().then(function(payments) {
ctrl.payments = payments;
});
Mandates.findAll().then(function(mandates) {
ctrl.mandates = mandates;
});
}
]);
Why: What is being exported is clear and always done in one place, at the bottom of the file.
// Recommended
angular.module('organisationRolesNewControllerModule', [])
.controller('OrganisationRolesNewController', [
'permissions',
function CustomersShowController(permissions){
const ctrl = this;
function setAllPermissions(access) {
ctrl.form.permissions.forEach(function(permission) {
permission.access = access;
});
}
_.extend(ctrl, {
permissions: permissions,
setAllPermissions: setAllPermissions
});
}
]);
// Avoid
angular.module('organisationRolesNewControllerModule', [])
.controller('OrganisationRolesNewController', [
'permissions',
function CustomersShowController(permissions){
const ctrl = this;
ctrl.permissions = permissions;
ctrl.setAllPermissions = function setAllPermissions(access) {
ctrl.form.permissions.forEach(function(permission) {
permission.access = access;
});
}
}
]);
Why: Adding unused properties to the digest cycle is expensive.
// Recommended
angular.module('webhooksIndexControllerModule', [])
.controller('WebhooksIndexController', [
'TestWebhooks', 'AlertList', 'webhooks'
function WebhooksIndexController(TestWebhooks, AlertList, webhooks) {
const ctrl = this;
function success() {
AlertList.success('Your test webhook has been created and will be sent shortly');
}
function error() {
AlertList.error('Failed to send test webhook, please try again');
}
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(success, error);
}
_.extend(ctrl, {
webhooks: webhooks,
sendTestWebhook: sendTestWebhook
});
}
]);
// Avoid
angular.module('webhooksIndexControllerModule', [])
.controller('WebhooksIndexController', [
'TestWebhooks', 'AlertList', 'webhooks'
function WebhooksIndexController(TestWebhooks, AlertList, webhooks) {
const ctrl = this;
function success() {
AlertList.success('Your test webhook has been created and will be sent shortly');
}
function error() {
AlertList.error('Failed to send test webhook, please try again');
}
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(success, error);
}
_.extend(ctrl, {
webhooks: webhooks,
success: success,
error: error,
sendTestWebhook: sendTestWebhook
});
}
]);
Why:
// Recommended
angular.module('webhooksControllerModule', [])
.controller('WebhooksController', [
'TestWebhooks',
function WebhooksController(TestWebhooks) {
const ctrl = this;
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(function() {
$state.go('authRequired.organisation.roles.index', null);
AlertList.success('Your test webhook has been created and will be sent shortly');
});
}
_.extend(ctrl, {
sendTestWebhook: sendTestWebhook
});
}
]);
// Avoid
angular.module('webhooksControllerModule', [])
.controller('WebhooksController', [
'$http',
function WebhooksController($http) {
const ctrl = this;
function sendTestWebhook(webhook) {
$http({
method: 'POST',
data: { test_webhooks: webhook },
url: '/test_webhooks'
});
}
_.extend(ctrl, {
sendTestWebhook: sendTestWebhook
});
}
]);
Why: Allows reuse of controllers and encourages component encapsulation.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
<!-- Avoid -->
<div ng-controller='AlertListController as ctrl'>
<span>{{ ctrl.message }}</span>
</div>
Why: easier to see at the definition site and the call site exactly what function the service provides.
// Recommend
angular.module('buildCSVModule', []).factory('BuildCSV', function buildCVS() {
function build() {
// ...
}
return {
build: build,
}
});
// Avoid
angular.module('buildCSVModule', []).factory('BuildCSV', function buildCSV() {
function build() {
// ...
}
return build;
});
Why: Avoiding mutation in service objects makes it possible to reason about them from their call site without knowing what they do internally.
// Recommend
let events = [...];
events = EventPresenterService.present(events);
// Avoid
const events = [...];
EventPresenterService.present(events);
// events has been mutated
Use fromJS to convert a JS object into an Immutable type, and toJS at the end to convert back.
Why: ImmutableJS is a fantastic library for taking data and manipulating it without ever mutating.
// Recommend
angular.module('eventPresenterModule', []).factory('EventPresenterService', function eventPresenterService() {
function present(events) {
return Immutable.fromJS(events).map(function(event) {
event.set('some_data', true);
}).map(function(event) {
...
}).toJS();
}
return {
present: present,
}
});
lowerCamelCase and append Module.Why: A module name should be mapped to a file and clearly differentiated from constructors and service objects.
// Recommended
angular.module('usersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
// Avoid
angular.module('UsersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
Why:
// Recommended
angular.module('usersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
// Avoid
angular.module('app')
.controller('UsersPasswordEditController', []);
name property.Why:
// Recommended
import {passwordResetTokensModule} from 'app/services/password-reset-tokens/password-reset-tokens';
import {sessionModule} from 'app/services/session/session';
import {alertListModule} from 'app/components/alert-list/alert-list';
export const usersPasswordEditControllerModule = angular.module('usersPasswordEditControllerModule', [
passwordResetTokensModule.name,
sessionModule.name,
alertListModule.name
]);
// Avoid
import {passwordResetTokensModule} from 'app/services/password-reset-tokens/password-reset-tokens';
import {sessionModule} from 'app/services/session/session';
import {alertListModule} from 'app/components/alert-list/alert-list';
export const usersPasswordEditControllerModule = angular.module('usersPasswordEditControllerModule', [
'passwordResetTokensModule',
'sessionModule',
'alertListModule'
]);
Why: Makes it easier to edit directories.
// Current directory: app/services/creditors/
// Recommended
import {API_URL} from 'app/constants/api-url.constant';
import {authInterceptorModule} from 'app/services/auth-interceptor/auth-interceptor';
import {organisationIdInterceptorModule} from 'app/services/organisation-id-interceptor/organisation-id-interceptor';
// Avoid
import {API_URL} from '../../constants/api-url.constant';
import {authInterceptorModule} from '../services/auth-interceptor/auth-interceptor';
import {organisationIdInterceptorModule} from '../services/organisation-id-interceptor/organisation-id-interceptor';
Why: Avoids unnecessary expensive $watchers.
<!-- Recommended -->
<p>Name: {{::ctrl.name}}</p>
<!-- Avoid -->
<p>Name: {{ctrl.name}}</p>
ngInit – use controllers instead.<div ng-controller="Controller"> syntax. Use directives instead.Our applications are covered by two different types of test:
To write our tests we use Jasmine BDD and ngMock.
Every component should have a comprehensive set of unit tests.
Tests should be grouped into logical blocks using Jasmine's describe function. Tests for a function should all be contained within a describe block, and describe blocks should also be used to describe different scenarios, or contexts:
describe('#update', function() {
describe('when the data is valid', function() {
it('shows the success message', function() {…});
});
describe('when the data is invalid', function() {
it('shows errors', function() {…});
});
});
Each component should have its dependencies stubbed in each test.
Inject the dependencies and the components being tested in a beforeEach function. This encapsulates each test's state, ensuring that they are independent, making them easier to reason about. Tests should never depend on being run in a specific order.
let SomeService;
beforeEach(inject(function($injector) {
SomeService = $injector.get('SomeService');
}));
When injecting controllers for a test, use the controller as syntax:
beforeEach(inject(function($injector, $controller) {
$controller('OrganisationController as ctrl', {…});
}));
Always create a new scope to pass into the controller:
let scope;
let organisation = {
name: 'GoCardless'
};
beforeEach(inject(function($injector, $controller) {
scope = $injector.get('$rootScope').$new();
$controller('OrganisationController as ctrl', {
$scope: scope,
organisation: organisation
});
}));
When stubbing an API request using $httpBackend, always respond with a correctly formatted object. These responses should be saved individually as .json files and imported using the SystemJS JSON plugin:
import updateFixture from 'app/services/roles/update.fixture.json!json';
$httpBackend.expectPUT('someurl.com').respond(201, updateFixture);
Rules that pertain to our application at large, not a specific part of Angular.
Use:
$timeout instead of setTimeout$interval instead of setInterval$window instead of window$document instead of document$http instead of $.ajax$q (promises) instead of callbacksWhy: This makes tests easier to follow and faster to run as they can be executed synchronously.
Always use array annotation for dependency injection and bootstrap with strictDi.
Why: Negates the need for additional tooling to guard against minification and strictDi throws an
error if the array (or $inject) syntax is not used.
// Recommended
angular.module('creditorsShowControllerModule', [])
.controller('CreditorsShowController', [
'creditor', 'payments', 'payouts',
function CreditorsShowController(creditor, payments, payouts) {
const ctrl = this;
_.extend(ctrl, {
creditor: creditor,
payments: payments,
payouts: payouts
});
}
]);
// Avoid
angular.module('creditorsShowControllerModule', [])
.controller('CreditorsShowController',
function CreditorsShowController(creditor, payments, payouts) {
const ctrl = this;
_.extend(ctrl, {
creditor: creditor,
payments: payments,
payouts: payouts
});
});
// Recommended
import {mainModule} from './main';
angular.element(document).ready(function() {
angular.bootstrap(document.querySelector('[data-main-app]'), [
mainModule.name
], {
strictDi: true
});
});
// Avoid
import {mainModule} from './main';
angular.element(document).ready(function() {
angular.bootstrap(document.querySelector('[data-main-app]'), [
mainModule.name
]);
});
$ name space in property names (e.g. $scope.$isActive = true).Why: Makes clear what is an Angular internal.
Why: Using DI makes testing and refactoring easier.
if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.Why: You should $scope.$apply() as close to the asynchronous event binding as possible.
We referred to lots of resources during the creation of this styleguide, including:
711 followers · starred Oct 2014
784 followers · starred Sep 2014
12,838 followers · starred Oct 2014
55 followers · starred Oct 2014
The principles we use to guide low-level decision making are:
import {dialogControllerModule} from './dialog.controller';
import template from './dialog.template.html!text';
_.extend could be replaced by Angular’s built in method angular.extend.$stateProvider could be replaced by $routeProvider.Note: For a complete setup see our Example ES6 Application Scaffold.
We organise our code as follows:
/app
/components
/alert
alert.directive.js
alert.directive.spec.js
alert.template.html
/config
main.config.js
/constants
api-url.constant.js
/routes
/customers
/index
customers-index.template.html
customers-index.route.js
customers-index.controller.js
customers-index.e2e.js
/helpers
/currency
currency-filter.js
currency-filter.spec.js
/unit
/e2e
/services
/creditors
creditors.js
creditors.spec.js
bootstrap.js
main.js
/assets
/fonts
/images
/stylesheets
404.html
index.html
Keep spec files in the same folder as the code being tested.
Components are encapsulated DOM components. Each component contains all the HTML, CSS, JavaScript, and other dependencies needed to render itself.
A view, made up of components and unique pieces of UI, that points to a URL. Like components, each route contains all the HTML, CSS, JavaScript, and other dependencies needed to render itself.
Services contain Business logic. For example, $http abstractions.
Configures Providers. For example, $locationProvider.html5Mode(true);.
Although JavaScript does not yet support constants, we run our application through Traceur, which supports const.
The constant should be named in all uppercase if it's a global constant that will be used across many different functions.
For example, export const API_URL = 'https://api.gocardless.com'.
If the constant is defined within a single function, it should be in regular camelCase.
Pure functions. For example, a currencyFilter might take in a number and format it for a certain currency. Helpers take input and return output without having any side effects.
Rules for using each of the core parts of AngularJS (routes, directives, controllers, modules, and templates).
Why: The page is rendered only when all data is available. This means views are only rendered once all the required data is available, and you avoid the user seeing any empty views whilst the data is loading.
// Recommended
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController',
controllerAs: 'ctrl',
resolve: {
customer: [
'Customers',
'$stateParams',
function customerResolver(Customers, $stateParams) {
return Customers.findOne({
params: { id: $stateParams.id }
});
}
]
}
});
// Avoid
// Note: Controller written inline for the example
$stateProvider.state('customers.show', {
url: '/customers/:id',
template: template,
controllerAs: 'ctrl',
controller: [
'Customers',
'$stateParams',
function CustomersShowController(Customers, $stateParams) {
const ctrl = this;
Customers.findOne({
params: { id: $stateParams.id }
}).then(function(customers) {
ctrl.customers = customers;
});
}
]
});
offset and limit when paginating.Why: The current view should be accurately reflected in the URL, which means any page refresh puts the user back in the exact state they were in.
// Recommended
function nextPage() {
const currentOffset = parseInt($stateParams.offset, 10) || 0;
const limit = parseInt($stateParams.limit, 10) || 10;
const nextOffset = currentOffset + limit;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}
// Avoid
// Keeping route state in memory only
let currentOffset = 0;
const limit = 10;
function nextPage() {
const nextOffset = currentOffset + limit;
currentOffset = nextOffset;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}
a-z and at least one dash (-).Why: Custom elements must have a dash (namespace) to differentiate them from native elements and prevent future component collisions.
<!-- Recommended -->
<dialog-box></dialog-box>
<button click-toggle="isActive"></button>
<!-- Avoid -->
<dialog></dialog>
<button toggle="isActive"></button>
Why: Separates responsibility: element directives add content; attribute directives add behaviour; class attributes add style.
<!-- Recommended -->
<alert-box message="Error"></alert-box>
<!-- Replaced with: -->
<alert-box message="Error" class="ng-isolate-scope">
<div class="alert-box">
<span class="alert-box__message">Error</span>
</div>
</alert-box>
<!-- Avoid -->
<p alert-box message="Error"></p>
<!-- Replaced with: -->
<p alert-box message="Error">
<div class="alert-box">
<span class="alert-box__message">Error</span>
</div>
</p>
<!-- Recommended -->
<button prevent-default="click">Submit</button>
<!-- Avoid -->
<prevent-default event="click">Submit</prevent-default>
Why: Using an isolate scope forces you to expose an API by giving the component all the data it needs. This increases reusability and testability. When using a shared scope for attribute directives you should not write to it or rely on any existing data. Attribute directives should not have an isolate scope because doing so overwrites the current scope.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E'
};
}
]);
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A'
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A',
scope: {}
};
}
]);
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A',
scope: true
};
}
]);
bindToController.Why: It explicitly shows what variables are shared via the controller.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
scope: {}
};
}
]);
$scope.$on('$destroy', ...) to get rid of any event listeners or DOM nodes created outside the directive element.Why: It avoids memory leaks and duplicate event listeners being bound when the directive is re-created.
// Recommended
angular.module('adminExpandComponentModule', [])
.directive('adminExpand', [
'$window',
function adminExpand($window) {
return {
restrict: 'A',
scope: {},
link: function adminExpandLink(scope, element) {
function expand() {
element.addClass('is-expanded');
}
$window.document.addEventListener('click', expand);
scope.$on('$destroy', function onAdminExpandDestroy() {
$window.document.removeEventListener('click', expand);
});
}
};
}
]);
// Avoid
angular.module('adminExpandComponentModule', [])
.directive('adminExpand', [
'$window',
function adminExpand($window) {
return {
restrict: 'A',
scope: {},
link: function adminExpandLink(scope, element) {
function expand() {
element.addClass('is-expanded');
}
$window.document.addEventListener('click', expand);
}
};
}
]);
x-, polymer-, ng-.controllerAs syntax.Why: It explicitly shows what controller a variable belongs to, by writing {{ ctrl.foo }} instead of {{ foo }}.
// Recommended
$stateProvider.state('authRequired.customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController',
controllerAs: 'ctrl'
});
// Avoid
$stateProvider.state('authRequired.customers.show', {
url: '/customers/:id',
template: template,
controller: 'CustomersShowController'
});
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
// Avoid
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
scope: {}
};
}
]);
Why:
// Recommended
angular.module('customersShowControllerModule', [])
.controller('CustomersShowController', [
'customer', 'payments', 'mandates',
function CustomersShowController(customer, payments, mandates){
const ctrl = this;
_.extend(ctrl, {
customer: customer,
payments: payments,
mandates: mandates
});
}
]);
// Avoid
angular.module('customersShowControllerModule', [])
.controller('CustomersShowController', [
'Customers', 'Payments', 'Mandates', '$stateParams',
function CustomersShowController(Customers, Payments, Mandates, $stateParams){
const ctrl = this;
Customers.findOne({
params: { id: $stateParams.id }
}).then(function(customers) {
ctrl.customers = customers;
});
Payments.findAll().then(function(payments) {
ctrl.payments = payments;
});
Mandates.findAll().then(function(mandates) {
ctrl.mandates = mandates;
});
}
]);
Why: What is being exported is clear and always done in one place, at the bottom of the file.
// Recommended
angular.module('organisationRolesNewControllerModule', [])
.controller('OrganisationRolesNewController', [
'permissions',
function CustomersShowController(permissions){
const ctrl = this;
function setAllPermissions(access) {
ctrl.form.permissions.forEach(function(permission) {
permission.access = access;
});
}
_.extend(ctrl, {
permissions: permissions,
setAllPermissions: setAllPermissions
});
}
]);
// Avoid
angular.module('organisationRolesNewControllerModule', [])
.controller('OrganisationRolesNewController', [
'permissions',
function CustomersShowController(permissions){
const ctrl = this;
ctrl.permissions = permissions;
ctrl.setAllPermissions = function setAllPermissions(access) {
ctrl.form.permissions.forEach(function(permission) {
permission.access = access;
});
}
}
]);
Why: Adding unused properties to the digest cycle is expensive.
// Recommended
angular.module('webhooksIndexControllerModule', [])
.controller('WebhooksIndexController', [
'TestWebhooks', 'AlertList', 'webhooks'
function WebhooksIndexController(TestWebhooks, AlertList, webhooks) {
const ctrl = this;
function success() {
AlertList.success('Your test webhook has been created and will be sent shortly');
}
function error() {
AlertList.error('Failed to send test webhook, please try again');
}
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(success, error);
}
_.extend(ctrl, {
webhooks: webhooks,
sendTestWebhook: sendTestWebhook
});
}
]);
// Avoid
angular.module('webhooksIndexControllerModule', [])
.controller('WebhooksIndexController', [
'TestWebhooks', 'AlertList', 'webhooks'
function WebhooksIndexController(TestWebhooks, AlertList, webhooks) {
const ctrl = this;
function success() {
AlertList.success('Your test webhook has been created and will be sent shortly');
}
function error() {
AlertList.error('Failed to send test webhook, please try again');
}
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(success, error);
}
_.extend(ctrl, {
webhooks: webhooks,
success: success,
error: error,
sendTestWebhook: sendTestWebhook
});
}
]);
Why:
// Recommended
angular.module('webhooksControllerModule', [])
.controller('WebhooksController', [
'TestWebhooks',
function WebhooksController(TestWebhooks) {
const ctrl = this;
function sendTestWebhook(webhook) {
TestWebhooks.create({
data: { test_webhooks: webhook }
}).then(function() {
$state.go('authRequired.organisation.roles.index', null);
AlertList.success('Your test webhook has been created and will be sent shortly');
});
}
_.extend(ctrl, {
sendTestWebhook: sendTestWebhook
});
}
]);
// Avoid
angular.module('webhooksControllerModule', [])
.controller('WebhooksController', [
'$http',
function WebhooksController($http) {
const ctrl = this;
function sendTestWebhook(webhook) {
$http({
method: 'POST',
data: { test_webhooks: webhook },
url: '/test_webhooks'
});
}
_.extend(ctrl, {
sendTestWebhook: sendTestWebhook
});
}
]);
Why: Allows reuse of controllers and encourages component encapsulation.
// Recommended
angular.module('alertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
controllerAs: 'ctrl',
bindToController: true,
template: template,
scope: {}
};
}
]);
<!-- Avoid -->
<div ng-controller='AlertListController as ctrl'>
<span>{{ ctrl.message }}</span>
</div>
Why: easier to see at the definition site and the call site exactly what function the service provides.
// Recommend
angular.module('buildCSVModule', []).factory('BuildCSV', function buildCVS() {
function build() {
// ...
}
return {
build: build,
}
});
// Avoid
angular.module('buildCSVModule', []).factory('BuildCSV', function buildCSV() {
function build() {
// ...
}
return build;
});
Why: Avoiding mutation in service objects makes it possible to reason about them from their call site without knowing what they do internally.
// Recommend
let events = [...];
events = EventPresenterService.present(events);
// Avoid
const events = [...];
EventPresenterService.present(events);
// events has been mutated
Use fromJS to convert a JS object into an Immutable type, and toJS at the end to convert back.
Why: ImmutableJS is a fantastic library for taking data and manipulating it without ever mutating.
// Recommend
angular.module('eventPresenterModule', []).factory('EventPresenterService', function eventPresenterService() {
function present(events) {
return Immutable.fromJS(events).map(function(event) {
event.set('some_data', true);
}).map(function(event) {
...
}).toJS();
}
return {
present: present,
}
});
lowerCamelCase and append Module.Why: A module name should be mapped to a file and clearly differentiated from constructors and service objects.
// Recommended
angular.module('usersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
// Avoid
angular.module('UsersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
Why:
// Recommended
angular.module('usersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
// Avoid
angular.module('app')
.controller('UsersPasswordEditController', []);
name property.Why:
// Recommended
import {passwordResetTokensModule} from 'app/services/password-reset-tokens/password-reset-tokens';
import {sessionModule} from 'app/services/session/session';
import {alertListModule} from 'app/components/alert-list/alert-list';
export const usersPasswordEditControllerModule = angular.module('usersPasswordEditControllerModule', [
passwordResetTokensModule.name,
sessionModule.name,
alertListModule.name
]);
// Avoid
import {passwordResetTokensModule} from 'app/services/password-reset-tokens/password-reset-tokens';
import {sessionModule} from 'app/services/session/session';
import {alertListModule} from 'app/components/alert-list/alert-list';
export const usersPasswordEditControllerModule = angular.module('usersPasswordEditControllerModule', [
'passwordResetTokensModule',
'sessionModule',
'alertListModule'
]);
Why: Makes it easier to edit directories.
// Current directory: app/services/creditors/
// Recommended
import {API_URL} from 'app/constants/api-url.constant';
import {authInterceptorModule} from 'app/services/auth-interceptor/auth-interceptor';
import {organisationIdInterceptorModule} from 'app/services/organisation-id-interceptor/organisation-id-interceptor';
// Avoid
import {API_URL} from '../../constants/api-url.constant';
import {authInterceptorModule} from '../services/auth-interceptor/auth-interceptor';
import {organisationIdInterceptorModule} from '../services/organisation-id-interceptor/organisation-id-interceptor';
Why: Avoids unnecessary expensive $watchers.
<!-- Recommended -->
<p>Name: {{::ctrl.name}}</p>
<!-- Avoid -->
<p>Name: {{ctrl.name}}</p>
ngInit – use controllers instead.<div ng-controller="Controller"> syntax. Use directives instead.Our applications are covered by two different types of test:
To write our tests we use Jasmine BDD and ngMock.
Every component should have a comprehensive set of unit tests.
Tests should be grouped into logical blocks using Jasmine's describe function. Tests for a function should all be contained within a describe block, and describe blocks should also be used to describe different scenarios, or contexts:
describe('#update', function() {
describe('when the data is valid', function() {
it('shows the success message', function() {…});
});
describe('when the data is invalid', function() {
it('shows errors', function() {…});
});
});
Each component should have its dependencies stubbed in each test.
Inject the dependencies and the components being tested in a beforeEach function. This encapsulates each test's state, ensuring that they are independent, making them easier to reason about. Tests should never depend on being run in a specific order.
let SomeService;
beforeEach(inject(function($injector) {
SomeService = $injector.get('SomeService');
}));
When injecting controllers for a test, use the controller as syntax:
beforeEach(inject(function($injector, $controller) {
$controller('OrganisationController as ctrl', {…});
}));
Always create a new scope to pass into the controller:
let scope;
let organisation = {
name: 'GoCardless'
};
beforeEach(inject(function($injector, $controller) {
scope = $injector.get('$rootScope').$new();
$controller('OrganisationController as ctrl', {
$scope: scope,
organisation: organisation
});
}));
When stubbing an API request using $httpBackend, always respond with a correctly formatted object. These responses should be saved individually as .json files and imported using the SystemJS JSON plugin:
import updateFixture from 'app/services/roles/update.fixture.json!json';
$httpBackend.expectPUT('someurl.com').respond(201, updateFixture);
Rules that pertain to our application at large, not a specific part of Angular.
Use:
$timeout instead of setTimeout$interval instead of setInterval$window instead of window$document instead of document$http instead of $.ajax$q (promises) instead of callbacksWhy: This makes tests easier to follow and faster to run as they can be executed synchronously.
Always use array annotation for dependency injection and bootstrap with strictDi.
Why: Negates the need for additional tooling to guard against minification and strictDi throws an
error if the array (or $inject) syntax is not used.
// Recommended
angular.module('creditorsShowControllerModule', [])
.controller('CreditorsShowController', [
'creditor', 'payments', 'payouts',
function CreditorsShowController(creditor, payments, payouts) {
const ctrl = this;
_.extend(ctrl, {
creditor: creditor,
payments: payments,
payouts: payouts
});
}
]);
// Avoid
angular.module('creditorsShowControllerModule', [])
.controller('CreditorsShowController',
function CreditorsShowController(creditor, payments, payouts) {
const ctrl = this;
_.extend(ctrl, {
creditor: creditor,
payments: payments,
payouts: payouts
});
});
// Recommended
import {mainModule} from './main';
angular.element(document).ready(function() {
angular.bootstrap(document.querySelector('[data-main-app]'), [
mainModule.name
], {
strictDi: true
});
});
// Avoid
import {mainModule} from './main';
angular.element(document).ready(function() {
angular.bootstrap(document.querySelector('[data-main-app]'), [
mainModule.name
]);
});
$ name space in property names (e.g. $scope.$isActive = true).Why: Makes clear what is an Angular internal.
Why: Using DI makes testing and refactoring easier.
if (!$scope.$$phase) $scope.$apply(), it means your $scope.$apply() isn't high enough in the call stack.Why: You should $scope.$apply() as close to the asynchronous event binding as possible.
We referred to lots of resources during the creation of this styleguide, including:
711 followers · starred Oct 2014
784 followers · starred Sep 2014
12,838 followers · starred Oct 2014
55 followers · starred Oct 2014