- Prioritise readability and clarity.
- Be explicit, not implicit.
- Composability > inheritance.
- Know when to deviate from the style guide.
- Embrace the future of web application development – ES6 and Web Components (Angular 2.0).
Specs are kept in the same folder as the unit/route being tested.
Encapsulated DOM components. Each component contains all the HTML, CSS and JavaScript, and declares any dependencies needed to render itself.
Provider configuration. For example $locationProvider.html5Mode(true);.
For example export var API = 'https://api.gocardless.com';.
Pure functions. For example, currencyFilter.
A view tied to a URL. Each route contains all the HTML, CSS and JavaScript, and declares any service and component dependencies needed to render itself.
Business logic. For example $http abstractions.
/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
- Use resolvers to inject data. Why: The page is rendered only when all data is available.
// 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) {
var ctrl = this;
Customers.findOne({
params: { id: $stateParams.id }
}).then(function(customers) {
ctrl.customers = customers;
});
}
]
});- Use query parameters to store route state. For example, the current
offsetandlimitwhen paginating. Why: The current view should be represented in the URL.
// Recommended
function nextPage() {
var currentOffset = parseInt($stateParams.offset, 10) || 0;
var limit = parseInt($stateParams.limit, 10) || 10;
var nextOffset = currentOffset + limit;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}
// Avoid
// Keeping route state in memory only
var currentOffset = 0;
var limit = 10;
function nextPage() {
var nextOffset = currentOffset + limit;
currentOffset = nextOffset;
Payments.findAll({
params: { customers: $stateParams.id, limit: limit, offset: nextOffset }
});
}- Use element directives when content is injected. Else use attribute directives. Why: Separates responsibility: element directives add content, attribute directives add behaviour, and class attributes add style.
<!-- Recommended -->
<alert message="Error" />
<!-- Replaced with: -->
<div class="alert">
<span class="alert__message">Error</span>
</div>
<!-- Avoid -->
<p alert message="Error">
</p>
<!-- Replaced with: -->
<p alert message="Error">
<div class="alert">
<span class="alert__message">Error</span>
</div>
</p><!-- Recommended -->
<button prevent-default="click">Submit</button>
<!-- Avoid -->
<prevent-default event="click">Submit</prevent-default>- Use an isolate scope for element directives. Use inherited scope for attribute directives. Why: Using an isolate scope forces you to expose an API by giving the component all the data it needs, making it reusable and testable. Attribute directives should not have an isolate scope because doing so clobbers 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',
scope: true
};
}
]);
// Avoid
angular.module('AlertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'A'
};
}
]);- When using isolate-scope properties, always
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,
replace: true,
scope: {}
};
}
]);
// Avoid
angular.module('AlertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
replace: true,
scope: {}
};
}
]);- Tear down directives, subscribe to
$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);
}
};
}
]);- Don't rely on jQuery selectors. Use directives to target elements instead.
- Don't use jQuery to generate templates or DOM. Use directive templates instead.
- Use
controllerAssyntax. 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,
replace: true,
scope: {}
};
}
]);
// Avoid
angular.module('AlertListComponentModule', [])
.directive('alertList', [
function alertListDirective() {
return {
restrict: 'E',
controller: 'AlertListController',
template: template,
replace: true,
scope: {}
};
}
]);- Inject ready data instead of loading it in the controller. Why:
- 2.1. Simplifies testing with mock data.
- 2.2. Separates concerns: data is resolved in the route and used in the controller.
// Recommended
angular.module('CustomersShowControllerModule', [
]).controller('CustomersShowController', [
'customer', 'payments', 'mandates',
function CustomersShowController(customer, payments, mandates){
var ctrl = this;
_.extend(ctrl, {
customer: customer,
payments: payments,
mandates: mandates
});
}
]);
// Avoid
angular.module('CustomersShowControllerModule', [
]).controller('CustomersShowController', [
'Customers', 'Payments', 'Mandates',
function CustomersShowController(Customers, Payments, Mandates){
var 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;
});
}
]);- Extend a controller’s properties onto the controller. Why: What is being exported is clear.
// Recommended
angular.module('OrganisationRolesNewControllerModule', [
]).controller('OrganisationRolesNewController', [
'permissions',
function CustomersShowController(permissions){
var 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){
var ctrl = this;
ctrl.permissions = permissions;
ctrl.setAllPermissions = function setAllPermissions(access) {
ctrl.form.permissions.forEach(function(permission) {
permission.access = access;
});
}
}
]);- Only extend the controller with properties used in templates. Why: Adding unused properties to the digest cycle is expensive.
// Recommended
angular.module('WebhooksIndexControllerModule', [])
.controller('WebhooksIndexController', [
'TestWebhooks', 'AlertList', 'webhooks'
function WebhooksIndexController(TestWebhooks, AlertList, webhooks) {
var 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) {
var 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
});
}
]);- Store presentation logic in controllers and business logic in services. Why:
- 5.1. Simplifies testing business logic.
- 5.2. Controllers are glue code, and therefore require integration tests not unit tests.
// Recommended
// Avoid- Only instantiate controllers through routes or directives. Why: Allows reuse of controllers and encourages component encapsulation.
// Recommended
angular.module('WebhooksControllerModule', [])
.controller('WebhooksController', [
'TestWebhooks',
function WebhooksController(TestWebhooks) {
var 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) {
var ctrl = this;
function sendTestWebhook(webhook) {
$http({
method: 'POST',
data: { test_webhooks: webhook },
url: '/test_webhooks'
});
}
_.extend(ctrl, {
sendTestWebhook: sendTestWebhook
});
}
]);- Don’t manipulate DOM in your controllers, this will make your controllers harder for testing. Use directives instead.
- Create one module per file and don’t alter a module other than where it is defined. Why:
- 1.1. Prevents polluting the global scope.
- 1.2. Simplifies unit testing by declaring all dependencies needed to run each module.
- 1.3. Negates necessity to load files in a specific order.
// Recommended
angular.module('UsersPasswordEditControllerModule', [])
.controller('UsersPasswordEditController', []);
// Avoid
angular.module('app')
.controller('UsersPasswordEditController', []);- Use ES6 module system and reference other modules using Angular Module’s
nameproperty. Why:
- 2.1. Encapsulates all required files, making unit testing easier and error feedback more specific.
- 2.2. Simplifies upgrading to Angular 2.0, which uses ES6 modules.
// 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 var 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 var UsersPasswordEditControllerModule = angular.module('UsersPasswordEditControllerModule', [
'PasswordResetTokensModule',
'SessionModule',
'AlertListModule'
]);- Use relative imports only when importing from the current directory or any of its children. Use absolute paths when referencing modules in parent directories. 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';- Use one-time binding syntax when rendered data does not change during use.
Why: Avoids unnecessary and potentially expensive
$watchers.
<!-- Recommended -->
<p>Name: {{::ctrl.name}}</p>
<!-- Avoid -->
<p>Name: {{ctrl.name}}</p>- Don’t use
ngInit- use controllers instead. - Don’t use
<div ng-controller="Controller">syntax. Use directives instead.
Use:
$timeoutinstead ofsetTimeout$intervalinstead ofsetInterval$windowinstead ofwindow$documentinstead ofdocument$httpinstead of$.ajax$q(promises) instead of callbacks
Why: This makes your tests easier to follow and faster to run as they can be executed synchronously.
Always use the array annotation for dependency injection and bootstrap with strictDi.
Why: No need for additional tooling to guard against minification and strictDi throws an
error if you the array (or $inject) syntax is not used.
// Recommended
angular.module('CreditorsShowControllerModule', [
]).controller('CreditorsShowController', [
'creditor', 'payments', 'payouts',
function CreditorsShowController(creditor, payments, payouts) {
var ctrl = this;
_.extend(ctrl, {
creditor: creditor,
payments: payments,
payouts: payouts
});
}
]);
// Avoid
angular.module('CreditorsShowControllerModule', [
]).controller('CreditorsShowController',
function CreditorsShowController(creditor, payments, payouts) {
var 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
]);
});- Don’t use the
$name space in property names (e.g.$scope.$isActive = true). Why: Separates Angular internals.
2. Don't use globals. Resolve all dependencies using Dependency Injection. Why: Using DI makes testing and refactoring easier.
- Don't do
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 async event binding as possible.