Angular: Best Practices
Directory Structure
Group by feature, not by function
Components
app.js
components/
users/
users-service.js
users-directive.js
users-controller.js
users.scss
users.html
version/
version-directive.js
version-controller.js
version.scss
version.html
use the generator.
Dependency Injection
Be Explicit, @#$&!
Many ways to D.I.
angular.module('myApp', [])
.factory(MyFactory);
function MyService($dep1, dep2, dep3) { ... }
angular.module('myApp', [])
.factory(['$dep1', 'dep2', 'dep3', MyService]);
function MayService($dep1, dep2, dep3) { ... }
angular.module('myApp', [])
.factory(MyService);
MyService.$inject = ['$dep1', 'dep2', 'dep3']; //Function properties as decorators?
function MyService($dep1, dep2, dep3) { ... }
the "right" way ⤴
Alleviating the Tedium
angular.module('myApp', [])
.factory(MyService);
function MyService($dep1, dep2, dep3) {
'ngInject';
...
}
MWS uses the ng-annotate gulp plugin
Identify functions that need annotations with the 'ngInject';
prologue
use the generator!
Directives
Restrict directives to elements or attributes
app.directive('myComponent', myComponent);
function myComponent() {
return {
...
restrict: 'EA'
};
}
Only directives should manipulate DOM and only in a directive's `link` function.
app.directive('myComponent', myComponent);
function myComponent() {
return {
...
link: linkFn
};
function linkFn(scope, element, attributes) {
//DOM manipulation here only!
}
}
Aviod $scope.$watch at ALL costs.
app.directive('myComponent', myComponent);
function myComponent() {
return {
...
link: linkFn
};
function linkFn() {
scope.$watch('title', function (prevVal, currVal) {
//this is super taxing and will bog down your application
});
}
}
Use native event handlers instead.
app.module('my-app')
.controller('myCompCtrl', myCompCtrl);
function myCompCtrl() {
return {
onChange: function (evt) {
// react to change.
}
};
}
//my-component.html template
<input type="text" ng-change="onChange" />
React to changes in the directive's controller.
app.module('my-app')
.directive('myComp', myComp);
function myComp() {
return {
restrict: 'EA',
controller: myCompCtrl,
controllerAs: 'myCompCtrl'
bindToController: true,
link: linkFn,
}
};
}
Watchers
are $evil
Watchers are the methamphetamine of any angular application. It serves a purpose, but it's highly addictive and extremely damaging. The worst part is, out of the box, Angular acts more like a dealer than a pharmacist.
$digest is the problem. The $digest cycle is a loop over all your bindings that checks for changes and re-renders accordingly.
Bindings happen automatically not only for `{{ }}` but expressions including ng-if, ng-class, and most damning, ng-repeat
Your app has a fever, and the only prescription is...
One time binding
Angular 1.3 introduced a syntax for binding a value only once. This should be the default, and in fact, in Angular 2 it is.
Until then, ALWAYS use one time binding.
{{:: myCtrl.user }}
<div ng-if="::myCtrl.user"></div>
<div ng-class="::{ user: myCtrl.user }"></div>
<ol>
<li ng-repeat="user in ::myCtrl.users"></li>
</ol>
$scope
is $evil
$scope is an inheritance nightmare
Scopes are arranged in hierarchical structure which mimic the DOM structure of the application
- https://docs.angularjs.org/guide/scope
OMG wat?!?!?!
That tots sounds like a good idea.
<div class="show-scope-demo" ng-controller="GreetController">
<h1>Hello {{name}}</h1>
<div ng-controller="ListController">
<ol>
<li ng-repeat="name in names">
{{name}} from {{department}} says hi to {{$parent.name}}
</li>
</ol>
</div>
</div>
OMG! (Oh my gross!)
Just. Why? When you don't have to?
It gets worse
<div ng-controller="MainCtrl">
{{ title }}
<div ng-controller="AnotherCtrl">
Scope title: {{ title }}
MainCtrl's title: {{ $parent.title }}
<div ng-controller="YetAnotherCtrl">
{{ title }}
YetAnotherCtrl's title: {{ $parent.title }}
MainCtrl's title: {{ $parent.$parent.title }}
</div>
</div>
</div>
Each nested controller has to have knowledge of controller(s) it's composed with and at what depth.
controllerAs
FTLHL
<div ng-controller="MainCtrl as main">
{{ main.title }}
<div ng-controller="AnotherCtrl as another">
Scope title: {{ another.title }}
Parent title: {{ main.title }}
<div ng-controller="YetAnotherCtrl as yet">
Scope title: {{ yet.title }}
Parent title: {{ another.title }}
Parent parent title: {{ main.title }}
</div>
</div>
</div>
Child controllers can now reference parent controllers by their namespace, rather than through some convoluted $parent reference N times.
(For The Less Humiliating Loss)
controllerAs
FTLHL
app.controller('MainCtrl', MainCtrl);
MainCtrl.$inject = [];
function MainCtrl() {
this.title = 'Some title';
}
We're also able to treat our controller as a constructor and attach our context to this
(For The Less Humiliating Loss)
WARNING!!!
app.controller('MainCtrl', MainCtrl);
MainCtrl.$inject = [];
function MainCtrl() {
return {
title: 'Some title'
};
}
We could additionally compel our controller to act like a factory -- the way the rest of Angular methods do -- by returning an object.
Controversy ahead!!! Not advised for those with low blood pressure, those who are pregnant or may become pregnant, and the elderly.
Route controllerAs FTD
.when('/', {
templateUrl: 'components/main/main.html',
controllerAs: 'main',
controller: 'MainCtrl'
});
Better right? Our templates are more reusable, our controllers are more reusable, and we've separated our concerns much better.
(For The Draw)
FOUL!!!!!!
You can't do nested views with controllerAs in your route definition!
Nested Views
are $evil
Stop thinking of your application in terms of views that need to be loaded. That kind of thinking aligns better with imperative frameworks but doesn't work well in angular.
An Angular 2 application consists of nested components. So an Angular 2 application will always have a component tree.
- Victor Savkin: core Angular 2 developer
MVC
MV
MVVM
MV*
MC
Models
Components
App developers are responsible for updating models.
Angular reflects model changes into UI via components
Models
Components
Services,
Filters,
Factories
Views,
Directives,
Controllers
Components
component = directive + controller
app.directive('myComponent', myComponent);
function myComponent() {
return {
restrict: 'EA',
scope: true,
templateUrl: 'my-component.html',
controllerAs: 'comp',
controller: MyComponentCtrl
bindToController: true,
link: linkFn
};
}
Views
view = [[component]]
.when('/', {
templateUrl: 'views/main.html'
})
.when('/', {
template: '<my-home></my-home>'
})
Controllers
Controllers should only be for creating view-models
The idea here is to avoid logic in your controller.
The effect of this is that virtually all functions that you may be tempted to write in a controller, should instead by put into a service.
This also simplifies controller code and clarifies the role of a controller.
angular.module('my-app')
.controller('myCtrl', MyCtrl);
function MyCtrl() {
//cache our reference
var vm = this;
//This is cool
vm.firstName = 'Cory';
//This too is cool
vm.lastName = 'Brown';
//Oh noes! logic!!!
//Yes it's contrived. What do you want from me?
vm.fullName = function () {
return vm.firstName + ' ' + vm.lastName;
};
}
Behold, a contrived bad example.
angular.module('my-app')
.factory('fullNameinator', fullNameinator);
function fullNameinator() {
return function (first, last) {
//Oh yes, this is where logic goes!
return first + last;
}
}
angular.module('my-app').controller('myCtrl', MyCtrl);
MyCtrl.$inject = ['firstNameinator'];
function MyCtrl(fn) {
//cache our reference
var vm = this;
//This is cool
vm.firstName = 'Cory';
//This too is cool
vm.lastName = 'Brown';
//Now I'm cool too!
vm.fullName = fn(vm.firstName, vm.lastName);
}
Services
Services are singletons
.service
methods invoke
new
when they are called.
Since it is invoked with
new
, properties are attached via
this
.
Despite being a constructor, only one instance object is created for any given service. That one instance object is returned each time the service is injected.
angular.module('my-app')
.service('my-service', MyService);
function MyService() {
//this.now will always be the same time, when it was first instansiated
this.now = new Date();
}
Factories are singletons
.factory
methods do not invoke new
when they are called.
Because of this, an object needs to be returned. from the factory.
angular.module('my-app')
.factory('my-service', MyService);
function MyService() {
return {
//This too will always be the same time.
now = new Date();
};
}
Use .factory over .service
Like, pretty much always.
references
- Todd Moto: Opinionated AngularJS styleguide for teams
- Todd Motto: No $scope soup, bindToController in AngularJS
- Todd Motto: Killing it with Angular Directives; Structure and MVVM
- Victor Savkin: Two Phases Of Angular 2 Applications
- Jan Varwig: How to do nested views in AngularJS
- John Pappas: Angular Styleguide
Angular 1.4 best practices
By Cory Brown
Angular 1.4 best practices
- 2,192