Single page apps demand the front-end developers to become better software engineers. CSS and HTML are not the biggest concern anymore, in fact, there is no longer just a single concern. The front-end developer needs to handle XHRs, application logic (models, views, controllers), performance, animations, styles, structure, SEO, and integration with external services. The result which emerges from all those combined is the User Experience (UX) which should always be prioritized.
AngularJS is a very powerful framework. It is the third most starred repository on GitHub. It is not difficult to start using, but the goals that it is intended to accomplish demand comprehension. No longer can AngularJS developers ignore memory consumption, because it will not reset on navigation anymore. This is the vanguard of web development. Let’s embrace it!
Common Mistake #1: Accessing The Scope Through The DOM
There are a few optimization tweaks recommended for production. One of them is disabling debug info.
DebugInfoEnabled
is a setting which defaults to true, and allows for scope access through DOM nodes. If you want to try that through the JavaScript console, select a DOM element and access its scope with:angular.element(document.body).scope()
It can be useful even when not using jQuery with its CSS, but should not be used outside of the console. The reason being that when
$compileProvider.debugInfoEnabled
is set to false, calling .scope()
on a DOM node will return undefined
.
That is one of the few recommended options for production.
Please note that you can still access the scope through the console, even when on production. Call
angular.reloadWithDebugInfo()
from the console and the app will do just that.Common Mistake #2: Not Having a Dot In There
You probably have read that if you were not having a dot in your ng-model, you were doing it wrong. When it regards inheritance, that statement is often true. Scopes have a prototypal model of inheritance, typical to JavaScript, and nested scopes are common to AngularJS. Many directives create child scopes such as
ngRepeat
, ngIf
, and ngController
. When resolving a model, the lookup starts on the current scope and goes through every parent scope, all the way to $rootScope
.
But, when setting a new value, what happens depends on what kind of model (variable) we want to change. If the model is a primitive, the child scope will just create a new model. But if the change is to a property of a model object, the lookup on parent scopes will find the referenced object and change its actual property. A new model would not be set on the current scope, so no masking would occur:
function MainController($scope) {
$scope.foo = 1;
$scope.bar = {innerProperty: 2};
}
angular.module(‘myApp’, [])
.controller('MainController', MainController);
<div ng-controller=“MainController">
<p>OUTER SCOPE:</p>
<p>{{ foo }}</p>
<p>{{ bar.innerProperty }}</p>
<div ng-if=“foo”> <!— ng-if creates a new scope —>
<p>INNER SCOPE</p>
<p>{{ foo }}</p>
<p>{{ bar.innerProperty }}</p>
<button ng-click="foo = 2”>Set primitive</button>
<button ng-click=“bar.innerProperty = 3”>Mutate object</button>
</div>
</div>
Clicking the button labelled “Set primitive” will set foo in the inner scope to 2, but will not change foo in the outer scope.
Clicking the button labelled “Change object” will change the bar property from the parent scope. Since there is no variable on the inner scope, no shadowing will happen, and the visible value for bar will be 3 in both scopes.
Another way to do this is to leverage the fact that the parent scopes and the root Scope are referenced from every scope. The
$parent
and $root
objects can be used to access the parent scope and $rootScope
, respectively, directly from the view. It may be a powerful way, but I am not a fan of it due to the problem with targeting a particular scope up the stream. There is another way to set and access properties specific to a scope - using the controllerAs
syntax.Common Mistake #3: Not Using controllerAs Syntax
The alternative and most efficient way to assign models to use a controller object instead of the injected $scope. Instead of injecting scope, we can define models like this:
function MainController($scope) {
this.foo = 1;
var that = this;
var setBar = function () {
// that.bar = {someProperty: 2};
this.bar = {someProperty: 2};
};
setBar.call(this);
// there are other conventions:
// var MC = this;
// setBar.call(this); when using 'this' inside setBar()
}
<div>
<p>OUTER SCOPE:</p>
<p>{{ MC.foo }}</p>
<p>{{ MC.bar.someProperty }}</p>
<div ng-if="test1">
<p>INNER SCOPE</p>
<p>{{ MC.foo }}</p>
<p>{{ MC.bar.someProperty }}</p>
<button ng-click="MC.foo = 3">Change MC.foo</button>
<button ng-click="MC.bar.someProperty = 5">Change MC.bar.someProperty</button>
</div>
</div>
This is much less confusing. Especially when there are many nested scopes, as can be the case with nested states.
There is more to the controllerAs syntax.
Common Mistake #4: Not Fully Utilising The controllerAs Syntax
There are a few caveats with how the controller object is exposed. It is basically an object set on the controller’s scope, just like a normal model.
If you need to watch a property of the controller object, you can watch a function but you are not required to. Here is an example:
function MainController($scope) {
this.title = 'Some title';
$scope.$watch(angular.bind(this, function () {
return this.title;
}), function (newVal, oldVal) {
// handle changes
});
}
It is easier to just do:
function MainController($scope) {
this.title = 'Some title';
$scope.$watch(‘MC.title’, function (newVal, oldVal) {
// handle changes
});
}
Meaning that also down the scope chain, you could access MC from a child controller:
function NestedController($scope) {
if ($scope.MC && $scope.MC.title === ‘Some title’) {
$scope.MC.title = ‘New title’;
}
}
However, to be able to do that you need to be consistent with the acronym you use for controllerAs. There are at least three ways to set it. You already saw the first one:
<div ng-controller=“MainController as MC”>
…
</div>
However, if you use
ui-router
, specifying a controller that way is prone to error. For states, controllers should be specified in the state configuration:angular.module(‘myApp’, [])
.config(function ($stateProvider) {
$stateProvider
.state('main', {
url: ‘/',
controller: ‘MainController as MC',
templateUrl: ‘/path/to/template.html'
})
}).
controller(‘MainController’, function () { … });
There is another way to annotate:
(…)
.state('main', {
url: ‘/',
controller: ‘MainController’,
controllerAs: ‘MC’,
templateUrl: ‘/path/to/template.html'
})
You can do the same in directives:
function AnotherController() {
this.text = 'abc';
}
function testForToptal() {
return {
controller: ‘AnotherController as AC',
template: '<p>{{ AC.text }}</p>'
};
}
angular.module(‘myApp', [])
.controller('AnotherController', AnotherController)
.directive('testForToptal', testForToptal);
The other way to annotate is valid too, although less concise:
function testForToptal() {
return {
controller: 'AnotherController',
controllerAs: 'AC',
template: '<p>{{ AC.text }}</p>'
};
}
Common Mistake #5: Not Using Named Views With UI-ROUTER For Power”
The de facto routing solution for AngularJS has been, until now, the
ui-router
. Removed from core sometime ago, ngRoute module, was too basic for more sophisticated routing.
There is a new
NgRouter
on it’s way, but the authors still consider it too early for production. When I am writing this, the stable Angular is 1.3.15, and ui-router
rocks.
The main reasons:
- awesome state nesting
- route abstraction
- optional and required parameters
Here I will cover state nesting to avoid AngularJS errors.
Think of this as a complex yet standard use case. There is an app, which has a homepage view and a product view. The product view has three separate sections: the intro, the widget, and the content. We want the widget to persist and not reload when switching between state. But the content should reload.
Consider the following HTML product index page structure:
<body>
<header>
<!-- SOME STATIC HEADER CONTENT -->
</header>
<section class="main">
<div class="page-content">
<div class="row">
<div class="col-xs-12">
<section class="intro">
<h2>SOME PRODUCT SPECIFIC INTRO</h2>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<section class="widget">
<!-- some widget, which should never reload -->
</section>
</div>
<div class="col-xs-9">
<section class="content">
<div class="product-content">
<h2>Product title</h2>
<span>Context-specific content</span>
</div>
</section>
</div>
</div>
</div>
</section>
<footer>
<!-- SOME STATIC HEADER CONTENT -->
</footer>
</body>
This is something we could get from the HTML coder, and now need to separate it into files and states. I generally go with the convention that there is an abstract MAIN state, which keeps the global data if needed. Use that instead of $rootScope. The Main state will also keep static HTML that is required on every page. I keep index.html clean.
<!— index.html —>
<body>
<div ui-view></div>
</body>
<!— main.html —>
<header>
<!-- SOME STATIC HEADER CONTENT -->
</header>
<section class="main">
<div ui-view></div>
</section>
<footer>
<!-- SOME STATIC HEADER CONTENT -->
</footer>
Then let’s see the product index page:
<div class="page-content">
<div class="row">
<div class="col-xs-12">
<section class="intro">
<div ui-view="intro"></div>
</section>
</div>
</div>
<div class="row">
<div class="col-xs-3">
<section class="widget">
<div ui-view="widget"></div>
</section>
</div>
<div class="col-xs-9">
<section class="content">
<div ui-view="content"></div>
</section>
</div>
</div>
</div>
As you can see, the product index page has three named views. One for the intro, one for the widget, and one for the product. We meet the specs! So now let’s set up routing:
function config($stateProvider) {
$stateProvider
// MAIN ABSTRACT STATE, ALWAYS ON
.state('main', {
abstract: true,
url: '/',
controller: 'MainController as MC',
templateUrl: '/routing-demo/main.html'
})
// A SIMPLE HOMEPAGE
.state('main.homepage', {
url: '',
controller: 'HomepageController as HC',
templateUrl: '/routing-demo/homepage.html'
})
// THE ABOVE IS ALL GOOD, HERE IS TROUBLE
// A COMPLEX PRODUCT PAGE
.state('main.product', {
abstract: true,
url: ':id',
controller: 'ProductController as PC',
templateUrl: '/routing-demo/product.html',
})
// PRODUCT DEFAULT SUBSTATE
.state('main.product.index', {
url: '',
views: {
'widget': {
controller: 'WidgetController as PWC',
templateUrl: '/routing-demo/widget.html'
},
'intro': {
controller: 'IntroController as PIC',
templateUrl: '/routing-demo/intro.html'
},
'content': {
controller: 'ContentController as PCC',
templateUrl: '/routing-demo/content.html'
}
}
})
// PRODUCT DETAILS SUBSTATE
.state('main.product.details', {
url: '/details',
views: {
'widget': {
controller: 'WidgetController as PWC',
templateUrl: '/routing-demo/widget.html'
},
'content': {
controller: 'ContentController as PCC',
templateUrl: '/routing-demo/content.html'
}
}
});
}
angular.module('articleApp', [
'ui.router'
])
.config(config);
That would be the first approach. Now, what happens when switching between
main.product.index
and main.product.details
? The content and widget get reloaded, but we only want to reload the content. This was problematic, and developers actually created routers that would support just that functionality. One of the names for this was sticky views. Fortunately, ui-router
supports that out of the box with absolute named view targeting.// A COMPLEX PRODUCT PAGE
// WITH NO MORE TROUBLE
.state('main.product', {
abstract: true,
url: ':id',
views: {
// TARGETING THE UNNAMED VIEW IN MAIN.HTML
'@main': {
controller: 'ProductController as PC',
templateUrl: '/routing-demo/product.html'
},
// TARGETING THE WIDGET VIEW IN PRODUCT.HTML
// BY DEFINING A CHILD VIEW ALREADY HERE, WE ENSURE IT DOES NOT RELOAD ON CHILD STATE CHANGE
'widget@main.product': {
controller: 'WidgetController as PWC',
templateUrl: '/routing-demo/widget.html'
}
}
})
// PRODUCT DEFAULT SUBSTATE
.state('main.product.index', {
url: '',
views: {
'intro': {
controller: 'IntroController as PIC',
templateUrl: '/routing-demo/intro.html'
},
'content': {
controller: 'ContentController as PCC',
templateUrl: '/routing-demo/content.html'
}
}
})
// PRODUCT DETAILS SUBSTATE
.state('main.product.details', {
url: '/details',
views: {
'content': {
controller: 'ContentController as PCC',
templateUrl: '/routing-demo/content.html'
}
}
});
By moving the state definition to the parent view, which also is abstract, we can preserve the child view from reloading when switching urls which normally affects that child’s siblings. Of course, the widget could be a simple directive. But the point is, it could also be another complex nested state.
There is another way to do this through the use of
$urlRouterProvider.deferIntercept()
, but I think that using state configuration is actually better. If you are interested in intercepting routes, I wrote a small tutorial on StackOverflow.Common Mistake #6: Declaring Everything In The Angular World Using Anonymous Functions
This mistake is of a lighter calibre, and is more a question of style than avoiding AngularJS error messages. You may have previously noticed that I seldom pass anonymous functions to angular internal’s declarations. I usually just define a function first and then pass it in.
This regards more than just functions. I got this approach from reading style guides, especially Airbnb’s and Todd Motto’s. I believe there are several advantages and almost no drawbacks to it.
First of all, you can manipulate and mutate your functions and objects much easier if they are assigned to a variable. Secondly, the code is cleaner and can be easily split into files. That means maintainability. If you don’t want to pollute the global namespace, wrap every file in IIFEs. The third reason is testability. Consider this example:
'use strict';
function yoda() {
var privateMethod = function () {
// this function is not exposed
};
var publicMethod1 = function () {
// this function is exposed, but it's internals are not exposed
// some logic...
};
var publicMethod2 = function (arg) {
// THE BELOW CALL CANNOT BE SPIED ON WITH JASMINE
publicMethod1('someArgument');
};
// IF THE LITERAL IS RETURNED THIS WAY, IT CAN'T BE REFERRED TO FROM INSIDE
return {
publicMethod1: function () {
return publicMethod1();
},
publicMethod2: function (arg) {
return publicMethod2(arg);
}
};
}
angular.module('app', [])
.factory('yoda', yoda);
So now we could mock the
publicMethod1
, but why should we do that since it is exposed? Wouldn’t it be easier just to spy on the existing method? However, the method is actually another function - a thin wrapper. Take a look at this approach:function yoda() {
var privateMethod = function () {
// this function is not exposed
};
var publicMethod1 = function () {
// this function is exposed, but it's internals are not exposed
// some logic...
};
var publicMethod2 = function (arg) {
// the below call cannot be spied on
publicMethod1('someArgument');
// BUT THIS ONE CAN!
hostObject.publicMethod1('aBetterArgument');
};
var hostObject = {
publicMethod1: function () {
return publicMethod1();
},
publicMethod2: function (arg) {
return publicMethod2(arg);
}
};
return hostObject;
}
This is not only about style, since in effect the code is more reusable and idiomatic. The developer gets more expressive power. Splitting all code into self-contained blocks just makes it easier.
Common Mistake #7: Doing Heavy Processing In Angular AKA Using Workers
In some scenarios, it may be required to process a large array of complex objects by passing them through a set of filters, decorators, and finally a sorting algorithm. One use case is when the app should work offline or where the performance of displaying data is key. And since JavaScript is single-threaded, it is relatively easy to freeze the browser.
It is also easy to avoid it with web workers. There don’t seem to be any popular libraries that handle that specifically for AngularJS. It might be for the best though, since the implementation is easy.
First, let’s setup the service:
function scoringService($q) {
var scoreItems = function (items, weights) {
var deferred = $q.defer();
var worker = new Worker('/worker-demo/scoring.worker.js');
var orders = {
items: items,
weights: weights
};
worker.postMessage(orders);
worker.onmessage = function (e) {
if (e.data && e.data.ready) {
deferred.resolve(e.data.items);
}
};
return deferred.promise;
};
var hostObject = {
scoreItems: function (items, weights) {
return scoreItems(items, weights);
}
};
return hostObject;
}
angular.module('app.worker')
.factory('scoringService', scoringService);
Now, the worker:
'use strict';
function scoringFunction(items, weights) {
var itemsArray = [];
for (var i = 0; i < items.length; i++) {
// some heavy processing
// itemsArray is populated, etc.
}
itemsArray.sort(function (a, b) {
if (a.sum > b.sum) {
return -1;
} else if (a.sum < b.sum) {
return 1;
} else {
return 0;
}
});
return itemsArray;
}
self.addEventListener('message', function (e) {
var reply = {
ready: true
};
if (e.data && e.data.items && e.data.items.length) {
reply.items = scoringFunction(e.data.items, e.data.weights);
}
self.postMessage(reply);
}, false);
Now, inject the service as usual and treat
scoringService.scoreItems()
as you would any service method that returns a promise. The heavy processing will be carried out on a separate thread, and no harm will be done to the UX.
What to look out for:
- there does not seem to be a general rule for how many workers to spawn. Some developers claim that 8 is a good number, but use an online calculator and suit yourself
- check compatibility with older browsers
- I run into an issue when passing the number 0 from the service to the worker. I applied .toString() on the passed property, and it worked correctly.
Common Mistake #8: Overusing And Misunderstanding Resolves
Resolves add extra time to the loading of the view. I believe that high performance of the front-end app is our primary goal. It should not be a problem to render some parts of the view while the app waits for the data from the API.
Consider this setup:
function resolve(index, timeout) {
return {
data: function($q, $timeout) {
var deferred = $q.defer();
$timeout(function () {
deferred.resolve(console.log('Data resolve called ' + index));
}, timeout);
return deferred.promise;
}
};
}
function configResolves($stateProvide) {
$stateProvider
// MAIN ABSTRACT STATE, ALWAYS ON
.state('main', {
url: '/',
controller: 'MainController as MC',
templateUrl: '/routing-demo/main.html',
resolve: resolve(1, 1597)
})
// A COMPLEX PRODUCT PAGE
.state('main.product', {
url: ':id',
controller: 'ProductController as PC',
templateUrl: '/routing-demo/product.html',
resolve: resolve(2, 2584)
})
// PRODUCT DEFAULT SUBSTATE
.state('main.product.index', {
url: '',
views: {
'intro': {
controller: 'IntroController as PIC',
templateUrl: '/routing-demo/intro.html'
},
'content': {
controller: 'ContentController as PCC',
templateUrl: '/routing-demo/content.html'
}
},
resolve: resolve(3, 987)
});
}
The console output will be:
Data resolve called 3
Data resolve called 1
Data resolve called 2
Main Controller executed
Product Controller executed
Intro Controller executed
Which basically means that:
— The resolves are executed asynchronously
— We can’t rely on an order of execution (or at least need to flex quite a bit)
— All the states are blocked until all resolves do their thing, even if they are not abstract.
This means that before the user sees any output, he/she must wait for all the dependencies. We need to have that data, sure, okay. If it absolutely necessary to have it before the view, put it in a
.run()
block. Otherwise, just make the call to the service from the controller and handle the half-loaded state gracefully. Seeing work in progress - and the controller is already executed, so it actually is progress - is better than having the app stall.Common Mistake #9: Not Optimizing The App - Three Examples
a) Causing too many digest loops, such as attaching sliders to models
This is a general problem that can result in AngularJS errors, but I will discuss it at the example of sliders. I was using this slider library, angular range slider, because I needed the extended functionality. That directive has this syntax in the minimal version:
<body ng-controller=“MainController as MC”>
<div range-slider
min="0”
max=“MC.maxPrice”
pin-handle="min”
model-max=“MC.price”
>
</div>
</body>
Consider the following code in the controller:
this.maxPrice = '100';
this.price = '55’;
$scope.$watch('MC.price', function (newVal) {
if (newVal || newVal === 0) {
for (var i = 0; i < 987; i++) {
console.log('ALL YOUR BASE ARE BELONG TO US');
}
}
});
So that works slow. The casual solution would be to set a timeout on the input. But that is not always handy, and sometimes we don’t really want to delay the actual model change in all cases.
So we will add a temporary model bound to change the working model on timeout:
<body ng-controller=“MainController as MC”>
<div range-slider
min="0”
max=“MC.maxPrice”
pin-handle="min”
model-max=“MC.priceTemporary”
>
</div>
</body>
and in the controller:
this.maxPrice = '100';
this.price = '55’;
this.priceTemporary = '55';
$scope.$watch('MC.price', function (newVal) {
if (!isNaN(newVal)) {
for (var i = 0; i < 987; i++) {
console.log('ALL YOUR BASE ARE BELONG TO US');
}
}
});
var timeoutInstance;
$scope.$watch('MC.priceTemporary', function (newVal) {
if (!isNaN(newVal)) {
if (timeoutInstance) {
$timeout.cancel(timeoutInstance);
}
timeoutInstance = $timeout(function () {
$scope.MC.price = newVal;
}, 144);
}
});
b) Not using $applyAsync
AngularJS does not have a polling mechanism to call $digest(). It is only executed because we use the directives (e.g. ng-click, input), services ($timeout, $http), and methods ($watch) which evaluate our code and call a digest afterwards.
What
.$applyAsync()
does is it delays the resolution of expressions until the next $digest() cycle, which is triggered after a 0 timeout, which actually is ~10ms. There are two ways to use applyAsync now. An automated way for $http requests, and a manual way for the rest.
To make all http requests that return in around the same time resolve in one digest, do:
mymodule.config(function ($httpProvider) {
$httpProvider.useApplyAsync(true);
});
The manual way shows how it actually works. Consider some function that runs on the callback to a vanilla JS event listener or a jQuery
.click()
, or some other external library. After it executes and changes models, if you didn’t already wrap it in an $apply()
you need to call $scope.$root.$digest()
($rootScope.$digest()
), or at least $scope.$digest()
. Otherwise, you will see no change.
If you do that multiple times in one flow, it might start running slow. Consider calling
$scope.$applyAsync()
on the expressions instead. It will set only call one digest cycle for all of them.
c) Doing heavy processing of images
If you experience bad performance, you can investigate the reason by using the Timeline from Chrome Developer Tools. I will write more about this tool in mistake #17. If your timeline graph is dominated with the color green after recording, your performance issues may be related to processing of images. This is not strictly related to AngularJS, but may happen on top of AngularJS performance issues (which would be mostly yellow on the graph). As front-end engineers, we need to think about the complete end project.
Take a moment to assess:
- Do you use parallax?
- Do you have several layers of content overlapping one another?
- Do you move your images around?
- Do you scale images (e.g. with background-size)?
- Do you resize images in loops, and perhaps cause digest loops on resize?
If you answered “yes” to at least three of the above, consider easing it. Perhaps you can serve various image sizes and not resize at all. Maybe you could add the “transform: translateZ(0)” force GPU processing hack. Or use requestAnimationFrame for handlers.
Common Mistake #10: jQuerying It - Detached DOM Tree
Many times you probably hear that it’s not recommended to use jQuery with AngularJS, and that it should be avoided. It is imperative to understand the reason behind these statements. There are at least three reasons, as far as I can see, but none of them are actual blockers.
Reason 1: When you execute jQuery code, you need to call
$digest()
yourself. For many cases, there is an AngularJS solution which is tailored for AngularJS and can be of better use inside Angular than jQuery (e.g. ng-click or the event system).
Reason 2: The method of thought about building the app. If you have been adding JavaScript to websites, which reload when navigating, you did not have to worry about memory consumption too much. With single-page apps, you do have to worry. If you don’t clean up, users who spend more than a few minutes on your app may experience growing performance issues.
Reason 3: Cleaning up is not actually the easiest thing to do and analyze. There is no way to call a garbage collector from the script (in the browser). You may end up with detached DOM trees. I created an example (jQuery is loaded in index.html):
<section>
<test-for-toptal></test-for-toptal>
<button ng-click="MC.removeDirective()">remove directive</button>
</section>
function MainController($rootScope, $scope) {
this.removeDirective = function () {
$rootScope.$emit('destroyDirective');
};
}
function testForToptal($rootScope, $timeout) {
return {
link: function (scope, element, attributes) {
var destroyListener = $rootScope.$on('destroyDirective', function () {
scope.$destroy();
});
// adding a timeout for the DOM to get ready
$timeout(function () {
scope.toBeDetached = element.find('p');
});
scope.$on('$destroy', function () {
destroyListener();
element.remove();
});
},
template: '<div><p>I AM DIRECTIVE</p></div>'
};
}
angular.module('app', [])
.controller('MainController', MainController)
.directive('testForToptal', testForToptal);
This is a simple directive that outputs some text. There is a button below it, which will just destroy the directive manually.
So when the directive is removed, there remains a reference to the DOM tree in scope.toBeDetached. In chrome dev tools, if you access the tab “profiles” and then “take heap snapshot”, you will see in the output:
You can live with a few, but it is bad if you have a ton. Especially if for some reason, like in the example, you store it on the scope. The whole DOM will be evaluated on every digest. The problematic detached DOM tree is the one with 4 nodes. So how can this be solved?
scope.$on('$destroy', function () {
// setting this model to null
// will solve the problem.
scope.toBeDetached = null;
destroyListener();
element.remove();
});
The detached DOM tree with 4 entries is removed!
In this example, the directive uses the same scope, and stores the DOM element on the scope. It was easier for me to demonstrate it that way. It does not always get that bad, as you could store it in a variable. However, it would still take up memory if any closure that had referenced that variable or any other from the same function scope lived on.
Common Mistake #11: Overusing Isolated Scope
Whenever you need a directive that you know will be used in a single place, or which you don’t expect to conflict with whatever environment it is used in, there is no need to use isolated scope. Lately, there is a trend to create reusable components, but did you know that core angular directives don’t use isolated scope at all?
There are two main reasons: you can’t apply two isolated scope directives to an element, and you may encounter issues with nesting / inheritance / event processing. Especially regarding transclusion - the effects may not be what you expect.
So this would fail:
<p isolated-scope-directive another-isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux”></p>
And even if you use just one directive, you will notice that neither the isolated scope models nor events broadcasted in isolatedScopeDirective will not be available to AnotherController. That being sad, you can flex and use transclusion magic to make it work - but for most use cases, there is no need to isolate.
<p isolated-scope-directive ng-if="MC.quux" ng-repeat="q in MC.quux">
<div ng-controller="AnotherController">
… the isolated scope is not available here, look: {{ isolatedModel }}
</div>
</p>
So, two questions now:
- How can you process parent scope models in a same-scope directive?
- How can you instantiate new model values?
There are two ways, in both of them you pass values to attributes. Consider this MainController:
function MainController($interval) {
this.foo = {
bar: 1
};
this.baz = 1;
var that = this;
$interval(function () {
that.foo.bar++;
}, 144);
$interval(function () {
that.baz++;
}, 144);
this.quux = [1,2,3];
}
That controls this view:
<body ng-controller="MainController as MC">
<div class="cyan-surface">
<h1 style="font-size: 21px;">Attributes test</h1>
<test-directive watch-attribute="MC.foo" observe-attribute="current index: {{ MC.baz }}"></test-directive>
</div>
</body>
Notice that “watch-attribute” is not interpolated. It all works, due to JS magic. Here is the directive definition:
function testDirective() {
var postLink = function (scope, element, attrs) {
scope.$watch(attrs.watchAttribute, function (newVal) {
if (newVal) {
// take a look in the console
// we can't use the attribute directly
console.log(attrs.watchAttribute);
// the newVal is evaluated, and it can be used
scope.modifiedFooBar = newVal.bar * 10;
}
}, true);
attrs.$observe('observeAttribute', function (newVal) {
scope.observed = newVal;
});
};
return {
link: postLink,
templateUrl: '/attributes-demo/test-directive.html'
};
}
Notice that
attrs.watchAttribute
is passed into scope.$watch()
without the quotation marks! That means what was actually passed to $watch was the string MC.foo
! It does work, however, because any string passed into $watch()
gets evaluated against the scope and MC.foo
is available on the scope. That is also the most common way that attributes are watched in AngularJS core directives.
See the code on github for the template, and look into
$parse
and $eval
for even more awesomeness.Common Mistake #12: Not Cleaning Up After Yourself - Watchers, Intervals, Timeouts And Variables
AngularJS does some work on your behalf, but not all. The following need to be manually cleaned up:
- Any watchers that are not bound to the current scope (e.g. bound to $rootScope)
- Intervals
- Timeouts
- Variables referencing DOM in directives
- Dodgy jQuery plugins, e.g. those that don’t have handlers reacting to the JavaScript
$destroy
event
If you don’t do that manually, you will encounter unexpected behaviour and memory leaks. Even worse - these will not be instantly visible, but they will creep up eventually. Murphy’s law.
Amazingly, AngularJS provides handy ways to deal with all of those:
function cleanMeUp($interval, $rootScope, $timeout) {
var postLink = function (scope, element, attrs) {
var rootModelListener = $rootScope.$watch('someModel', function () {
// do something
});
var myInterval = $interval(function () {
// do something in intervals
}, 2584);
var myTimeout = $timeout(function () {
// defer some action here
}, 1597);
scope.domElement = element;
$timeout(function () {
// calling $destroy manually for testing purposes
scope.$destroy();
}, 987);
// here is where the cleanup happens
scope.$on('$destroy', function () {
// disable the listener
rootModelListener();
// cancel the interval and timeout
$interval.cancel(myInterval);
$timeout.cancel(myTimeout);
// nullify the DOM-bound model
scope.domElement = null;
});
element.on('$destroy', function () {
// this is a jQuery event
// clean up all vanilla JavaScript / jQuery artifacts here
// respectful jQuery plugins have $destroy handlers,
// that is the reason why this event is emitted...
// follow the standards.
});
};
Notice the jQuery
$destroy
event. It is called like the AngularJS one, but it is handled separately. Scope $watchers will not react to the jQuery event.Common Mistake #13: Keeping Too Many Watchers
This should be quite simple now. There is one thing to understand here:
$digest()
. For every binding {{ model }}
, AngularJS creates a watcher. On every digest phase, each such binding is evaluated and compared against the previous value. That is called dirty-checking, and that’s what $digest does. If the value changed since the last check, the watcher callback is fired. If that watcher callback modifies a model ($scope variable), a new $digest cycle is fired (up to a maximum of 10) when an exception is thrown.
Browsers don’t have problems even with thousands of bindings, unless the expressions are complex. The common answer for “how many watchers are ok to have” is 2000.
So, how can we limit the number of watchers? By not watching scope models when we don’t expect them to change. It is fairly easy onwards from AngularJS 1.3, since one-time bindings are in core now.
<li ng-repeat=“item in ::vastArray”>{{ ::item.velocity }}</li>
After
vastArray
and item.velocity
are evaluated once, they will never change again. You can still apply filters to the array, they will work just fine. It is just that the array itself will not be evaluated. In many cases, that is a win.Common Mistake #14: Misunderstanding The Digest
This AngularJS error was already partly covered in mistakes 9.b and in 13. This is a more thorough explanation. AngularJS updates DOM as a result of callback functions to watchers. Every binding, that is the directive
{{ someModel }}
sets up watchers, but watchers are also set for many other directives like ng-if
and ng-repeat
. Just take a look at the source code, it is very readable. Watchers can also be set manually, and you have probably done that at least a few times yourself.
$watch()
’ers are bound to scopes. $Watchers can take strings, which are evaluated against the scope that the $watch() was bound to. They can also evaluate functions. And they also take callbacks. So, when
$rootScope.$digest()` is called, all the registered models (that is $scope variables) are evaluated and compared against their previous values. If the values don’t match, the callback to the $watch() is executed.
It is important to understand that even though a model’s value was changed, the callback does not fire until the next digest phase. It is called a “phase” for a reason - it can consist of several digest cycles. If only a watcher changes a scope model, another digest cycle is executed.
But $digest() is not polled for. It is called from core directives, services, methods, etc. If you change a model from a custom function that does not call
.$apply
, .$applyAsync
, .$evalAsync
, or anything else that eventually calls $digest()
, the bindings will not be updated.
By the way, the source code for
$digest()
is actually quite complex. It is nevertheless worth reading, as the hilarious warnings make up for it.Common Mistake #15: Not Relying On Automation, Or Relying On It Too Much
If you follow the trends within front end development and are a bit lazy - like me - then you probably try to not do everything by hand. Keeping track of all your dependencies, processing sets of files in different ways, reloading the browser after every file save - there is a lot more to developing than just coding.
So you may be using bower, and maybe npm depending on how you serve your app. There is a chance that you may be using grunt, gulp, or brunch. Or bash, which also is cool. In fact, you may have started your latest project with some Yeoman generator!
This leads to the question: do you understand the whole process of what your infrastructure really does? Do you need what you have, especially if you just spent hours trying to fix your connect webserver livereload functionality?
Take a second to assess what you need. All those tools are only here to aid you, there is no other reward for using them. The more experienced developers I talk to tend to simplify things.
Common Mistake #16: Not Running The Unit Tests In TDD Mode
Tests will not make your code free of AngularJS error messages. What they will do is assure that your team doesn’t run into regression issues all the time.
I am writing specifically about unit tests here, not because I feel they are more important than e2e tests, but because they execute much faster. I must admit that the process I am about to describe is a very pleasurable one.
Test Driven Development as an implementation for e.g. gulp-karma runner, basically runs all your unit tests on every file save. My favorite way to write tests is, I just write empty assurances first:
describe(‘some module’, function () {
it(‘should call the name-it service…’, function () {
// leave this empty for now
});
...
});
After that, I write or refactor the actual code, then I come back to the tests and fill in the assurances with actual test code.
Having a TDD task running in a terminal speeds up the process by about 100%. Unit tests execute in a matter of a few seconds, even if you have a lot of them. Just save the test file and the runner will pick it up, evaluate your tests, and provide feedback instantly.
With e2e tests, the process is much slower. My advice - split e2e tests up into test suites and just run one at a time. Protractor has support for them, and below is the code I use for my test tasks (I like gulp).
'use strict';
var gulp = require('gulp');
var args = require('yargs').argv;
var browserSync = require('browser-sync');
var karma = require('gulp-karma');
var protractor = require('gulp-protractor').protractor;
var webdriverUpdate = require('gulp-protractor').webdriver_update;
function test() {
// Be sure to return the stream
// NOTE: Using the fake './foobar' so as to run the files
// listed in karma.conf.js INSTEAD of what was passed to
// gulp.src !
return gulp.src('./foobar')
.pipe(karma({
configFile: 'test/karma.conf.js',
action: 'run'
}))
.on('error', function(err) {
// Make sure failed tests cause gulp to exit non-zero
// console.log(err);
this.emit('end'); //instead of erroring the stream, end it
});
}
function tdd() {
return gulp.src('./foobar')
.pipe(karma({
configFile: 'test/karma.conf.js',
action: 'start'
}))
.on('error', function(err) {
// Make sure failed tests cause gulp to exit non-zero
// console.log(err);
// this.emit('end'); // not ending the stream here
});
}
function runProtractor () {
var argument = args.suite || 'all';
// NOTE: Using the fake './foobar' so as to run the files
// listed in protractor.conf.js, instead of what was passed to
// gulp.src
return gulp.src('./foobar')
.pipe(protractor({
configFile: 'test/protractor.conf.js',
args: ['--suite', argument]
}))
.on('error', function (err) {
// Make sure failed tests cause gulp to exit non-zero
throw err;
})
.on('end', function () {
// Close browser sync server
browserSync.exit();
});
}
gulp.task('tdd', tdd);
gulp.task('test', test);
gulp.task('test-e2e', ['webdriver-update'], runProtractor);
gulp.task('webdriver-update', webdriverUpdate);
Common Mistake #17: Not Using The Available Tools
A - chrome breakpoints
Chrome dev tools allow you to point at a specific place in any of the files loaded into the browser, pause code execution at that point, and let you interact with all the variables available from that point. That is a lot! That functionality does not require you to add any code at all, everything happens in the dev tools.
Not only you get access to all the variables, you also see the call stack, print stack traces, and more. You can even configure it to work with minified files. Read about it here.
There are other ways you can get similar run-time access, e.g. by adding
console.log()
calls. But breakpoints are more sophisticated.
AngularJS also allows you to access scope through DOM elements (as long as debugInfo is enabled), and inject available services through the console. Consider the following in the console:
$(document.body).scope().$root
or point at an element in the inspector, and then:
$($0).scope()
Even if debugInfo is not enabled, you can do:
angular.reloadWithDebugInfo()
And have it available after reload:
To inject and interact with a service from the console, try:
var injector = $(document.body).injector();
var someService = injector.get(‘someService’);
B - chrome timeline
Another great tool that comes with dev tools is the timeline. That will allow you to record and analyse your app’s live performance as you are using it. The output shows, among others, memory usage, frame rate, and the dissection of the different processes that occupy the CPU: loading, scripting, rendering, and painting.
If you experience that your app’s performance degrades, you will most likely be able to find the cause for that through the timeline tab. Just record your actions which led to performance issues and see what happens. Too many watchers? You will see yellow bars taking a lot of space. Memory leaks? You can see how much memory was consumed over time on a graph.
A detailed description: https://developer.chrome.com/devtools/docs/timeline
C - inspecting apps remotely on iOS and Android
If you are developing a hybrid app or a responsive web app, you can access your device’s console, DOM tree, and all other tools available either through Chrome or Safari dev tools. That includes the WebView and UIWebView.
First, start your web server on host 0.0.0.0 so that it is accessible from your local network. Enable web inspector in settings. Then connect your device to your desktop and access your local development page, using your machine’s ip instead of the regular “localhost”. That is all it takes, your device should now be available to you from your desktop’s browser.
Here are the detailed instructions for Android And for iOS, unofficial guides are to be found easily through google.
I recently had some cool experience with browserSync. It works in a similar way to livereload, but it also actually syncs all browsers that are viewing the same page through browserSync. That includes user interaction such as scrolling, clicking on buttons, etc. I was looking at the iOS app’s log output while controlling the page on the iPad from my desktop. It worked nicely!
Common Mistake #18: Not Reading The Source Code On The NG-INIT Example
Ng-init
, from the sound of it, should be similar to ng-if
and ng-repeat
, right? Did you ever wonder why there is a comment in the docs that it should not be used? IMHO that was surprising! I would expect the directive to initialize a model. That’s also what it does, but… it is implemented in a different way, that is, it does not watch the attribute value. You don’t need to browse through AngularJS source code - let me bring it to you:var ngInitDirective = ngDirective({
priority: 450,
compile: function() {
return {
pre: function(scope, element, attrs) {
scope.$eval(attrs.ngInit);
}
};
}
});
Less than you would expect? Quite readable, besides the awkward directive syntax, isn’t it? The sixth line is what it is all about.
Compare it to ng-show:
var ngShowDirective = ['$animate', function($animate) {
return {
restrict: 'A',
multiElement: true,
link: function(scope, element, attr) {
scope.$watch(attr.ngShow, function ngShowWatchAction(value) {
// we're adding a temporary, animation-specific class for ng-hide since this way
// we can control when the element is actually displayed on screen without having
// to have a global/greedy CSS selector that breaks when other animations are run.
// Read: https://github.com/angular/angular.js/issues/9103#issuecomment-58335845
$animate[value ? 'removeClass' : 'addClass'](element, NG_HIDE_CLASS, {
tempClasses: NG_HIDE_IN_PROGRESS_CLASS
});
});
}
};
}];
Again, the sixth line. There is a
$watch
there, that’s what makes this directive dynamic. In the AngularJS source code, a big part of all the code are comments that describe code that was mostly readable from the beginning. I believe it is a great way to learn about AngularJS.Conclusion
This guide covering most common AngularJS mistakes is almost twice as long as the other guides. It turned out that way naturally. The demand for high quality JavaScript front end engineers is very high. AngularJS is so hot right now, and it has been holding a stable position among the most popular development tools for a few years. With AngularJS 2.0 on the way, it will probably dominate for years to come.
What is great about front-end development is that it is very rewarding. Our work is visible instantly, and people interact directly with the products we deliver. The time spent learning JavaScript, and I believe we should focus on the JavaScript language, is a very good investment. It is the language of the Internet. The competition is super strong! There is one focus for us - user experience. To be successful, we need to cover everything.
Source code used in these examples can be downloaded from GitHub. Feel free to download it and make it your own.
I wanted to give credits to four publishing developers that inspired me the most:
I also wanted to thank all the great people on FreeNode #angularjs and #javascript channels for many excellent conversations, and continuous support.
And finally, always remember:
// when in doubt, comment it out! :)
This article was originally posted on Toptal
No comments:
Post a Comment
your comments.....