Here is a tutorial for the 7 best practices for working with
$scopes
in AngularJS.Keep the $rootScope clean of variables
This rule is based on the idea that the
$rootScope
is a global variable and falls under the best practice that Global Variables Are Bad . Adding variables to the $rootScope
will result in source code that is difficult to maintain. The usage of those variables will be scattered across JavaScript files, directives, controllers and templates. It will also make it difficult to unit test source code and do refactoring.What’s the problem with the $rootScope?
The problem isn’t the
$rootScope
, but what people are doing with it.- Template variables are assigned to the
$rootScope
instead of using a$scope
. - Functions are assigned to
$rootScope
that perform business logic instead of using a controller. - The user’s session state and details are stored in the
$rootScope
when it should be handled by a directive. - Developers use the
$rootScope
as a way to share data across multiple directives, controllers and templates when there are already AngularJS methods for handling this. - The
$rootScope
is used as a global state manager for the application (example; current language, current user, etc.).
Generally, I argue that the
$rootScope
should be kept as untouched as possible. There have been times when I’ve added simple math functions or string functions (for example; startsWith), but generally speaking a well designed app shouldn’t need to use the $rootScope
.If you have to use the $rootScope, then how should you use it?
The
$rootScope
is the first scope created when the application starts, and it’s available for modification when theangular.module('myApp',[]).run(..)
method is executed. After this point all references to $rootScope
should treat it as an immutable object.
Here are some tips on using
$rootScope
.- Only add properties to the
$rootScope
that are static or constant. Anything that represents a changing state or a mutable value should have a corresponding directive or controller that manages it and uses a$scope
assigned to a DOM element. If you can’t assign it once to the$rootScope
and leave it alone, then it doesn’t belong in the$rootScope
. - If you can’t define the new
$rootScope
property at application startup then it doesn’t belong in the$rootScope
. - All
$rootScope
modifications should take place in one location. If there is ever a question about what a global$rootScope
variable does or why it was added, then the developer just has to go to one JavaScript file.
The most logical location is where you place your
angular.run()
method. This is the startup function that is executed by AngularJS when bootstrapping your application. It’s also a place where you can inject the $rootScope
as a dependency.var app = angular.module('myApp',[]);
app.run(function($rootScope) {
$rootScope.Math = Math;
});
I like to assign the Math object into the $rootScope. This gives you access to all the JavaScript Math functions like min() and max() which are missing from AngularJS expressions.
Do not attach functions to a $scope
AngularJS documentation, the tutorials and sample projects all show how you can use functions inside a template by assigning a closure function to a
$scope
variable. I will say that there isn’t anything wrong with doing this, but I’ve never seen it done properly and it leads to poorly maintainable code.
When writing code for a directive’s link function try to limit yourself to just data related activities. Such as watchers, DOM manipulation and template variables. Closure functions in the scope is a do not do that issue for me.
Directives were designed to have controllers that could be injected into child directives as an API service, but I’ve found that controllers are much better for referencing functions from the template.
The most common problems with
$scope
functions- Difficult to tell which functions should have been in a controller.
- The functions often use variables from outside the closure function (this tightly couples the template, closure function and the link function).
- The scope often contains functions that are both business logic, data processing and template specific (the lines between the view and business logic becomes very blurry).
- Adds to the complicity of a directive without improving maintainability.
- Developers often re-use the same function names like
save()
,open()
andupdate()
and this makes the connection between a template and directive difficult to see.
Maintainability should be a good enough reason on it’s own not to use
$scope
functions. I’ll show you some examples of what I mean.Using a function from a controller
<div ng-controller="InventoryController as inventory">
<button ng-click="inventory.Save()">Save</button>
</div>
Or using a closure function assigned to the scope.
<div inventory>
<button ng-click="Save()">Save</button>
</div>
Which of the two examples above is better?
In the second example the Directive
inventory
has assigned a closure function named Save
to the $scope
, but we can’t be 100% sure until we actually look at the source code for that directive. That’s the problem with scopes as any directive in the DOM hierarchy could have assigned that closure function. One challenge with AngularJS is that the HTML templates does not express scope hierarchy. This means that looking at the HTML templates don’t tell you which directives are isolated scopes, inherited scopes or new scopes.Controller functions are better
Where as, with the
InventoryController
it’s perfectly clear that it’s method Save()
is being called. We can open the source code for that controller and begin working on that method immediately. The name of the controller as inventory
can act as a namespace inside the HTML templates helping to assist the developer in isolating business logic from view variables.
That’s why scope closure functions reduce maintainability and should be avoided. On small projects it’s no big deal but once you get into hundreds of directives and everyone is using the same function names like
Save()
. Well things start to get confusing and refactoring becomes a high risk activity.Try to use isolated scopes
AngularJS scopes are all automatically inherited from a parent scope. As the application grows in complexity so do the scopes grow in hierarchy, and what was a simple collection of scopes becomes a complex sea of inherited scopes.
Why does this happen?
It’s because AngularJS default for directives is to inherit scopes from their parent scope, and it’s also easier for new AngularJS developers to not worry about the difficulties associated with isolated scopes. So they just use the defaults.
This is where the developer becomes tempted to just modify a scope property that was created by a directive higher up the scope hierarchy, and this is a very bad thing to do. Here are some of the reasons why.
- It creates a tight coupling between two directives that is not enforced by AngularJS or any run-time rules.
- When debugging the application it appears that mysterious things are happening to a scope property. Finding the location where it was modified is difficult.
- It has the same disadvantages as global variables.
So isolate your scopes and try to create directives that can be used independently from each other. Only define dependencies that AngularJS will enforce for you (such as
require
). Avoid the temptation to use the current scope and bleed properties into child elements in the template. Instead, use the transclude
feature and an isolated scope.
Directives that need to know the values of parent scope properties can receive those values just as easily via HTML attributes and an isolated scope.
When to NOT use isolated scopes
There are some types of directives that simply can’t use isolated scopes. The most common type are attributedirectives that will be used on a HTML element that already has an isolated scope.
You can create
<input>
field validators as an example that don’t use an isolated scope.
These kinds of directives should be programmed with the understanding that the
$scope
is owned by another directive. A well designed directive will provide a controller that you can require
to call methods instead of directly modifying the $scope
.Use data objects for primitive types
You can save yourself a lot of headache, debugging and bugs by following one simple rule. Always define a
data object
to store values for your templates. It’s something that should become a habit by all AngularJS developers, because it solves so many common problems and makes templates easier to read.
Here’s an example of what happens when you don’t use a data object to hold primitive values.
app.directive('inventory',function(){
return {
scope: {},
link: function($scope, $el) {
$scope.title = 'Space Widgets';
$scope.amount = 9.99;
$scope.visible = false;
},
templateUrl: 'inventory.html'
});
<script type="text/ng-template" id="inventory.html">
<div ng-if="visible">
<h1>{{title}}</h1>
<input type="number" ng-model="amount"/>
</div>
</script>
In the above example the template will render the
<h1>{{title}}</h1>
correctly but the <input>
will appear to be broken. To an inexperienced AngularJS developer this will appear to be a mysterious bug and I’ve seen this cost people valuable time to fix.
When you are using any directives that require a 2-way data binding like
ng-model
it will not work on primitive variables when a new child scope is created. That is what happen when ng-if="visible"
was added to the template. Had the scope been using a data object from the beginning nothing would have broken when the change was made.
Here is the same example modified to use a data object called
model
.app.directive('inventory',function(){
return {
scope: {},
link: function($scope, $el) {
$scope.model = {
title: 'Space Widgets',
amount: 9.99,
visible: = false
};
},
templateUrl: 'inventory.html'
});
<script type="text/ng-template" id="inventory.html">
<div ng-if="model.visible">
<h1>{{model.title}}</h1>
<input type="number" ng-model="model.amount"/>
</div>
</script>
Things are now going to be stable when making changes to the template.
Don’t forget to $destroy
AngularJS handles the unbinding of event listeners in the most part automatically for you.
When binding to DOM elements outside the hierarchy of the directive. Those event listeners will remain active after the directive has been destroyed. So you have to remove them when
$destroy
is fired.
The following example of source code illustrates this common problem. The source code listens for window resize events, but leaves the event handler attached after the directive is destroyed.
app.directive('resizable',function(){
return {
link: function($scope, $el) {
$(window).bind('resize',function(){
// do some resize stuff
});
}
});
You have to
unbind
listeners you attach to DOM elements outside of $el
.app.directive('resizable',function($window){
return {
link: function($scope, $el) {
angular.element($window).bind('resize.'+$scope.$id,function(){
// do some resize stuff
});
$scope.$on('$destroy',function(){
angular.element($window).unbind('resize.'+$scope.$id);
});
}
});
Try using $scope.$watch
instead of element.bind
Here is something that I see often in AngularJS projects.
$(window).bind('resize.',function(){
$scope.width = window.innerWidth;
$scope.height = window.innerHeight;
$scope.$apply();
});
In the above example a third-party library is used to listen for events from outside of AngularJS. The event callback function is executed, the scope is updated and a call to
$scope.$apply()
is required for the changes to take effect.
So what’s the problem?
- It tightly couples digesting of the scope to an outside event.
- It forces AngularJS to digest everything from the $rootScope down.
- The scope is digested even if the directive didn’t need to be updated.
- The event listener has to be unbind when the scope is destroyed.
- These kinds of event listeners can create an accumulative performance problem.
There are going to be times when you need to do the above, but it should be your last resort. It’s better to use
$scope.$watch
to monitor the state of outside objects, and update the $scope
only when AngularJS is doing it’s regular digest cycle. In the majority of cases the above event listeners are not required.
Here’s how to do the same as above using
$scope.$watchGroup
.$scope.$watchGroup(function() {
return [$window.innerWidth,$window.innerHeight];
},function(values) {
$scope.width = values[0];
$scope.height = values[1];
});
Here’s the advantages:
- It doesn’t add any extra work for AngularJS
- It only updates the scope when the directive is digested.
- The watcher function is called only when the values change.
- It creates a directive that is data driven instead of event driven.
Document your $scope
properties with jsDoc
jsDoc is a markup language used to annotate JavaScript source code, and I’m a strong advocate for it’s usage. There are several popular IDE editors that now supports jsDoc. Providing the developer with intellisense feedback and autocomplete features. When you work with
$scope
objects it’s difficult to tell what’s been defined already, what $scope
object you’re using and where in the source code it was first assigned to $scope
. This is where jsDoc can be a benefit.
Start from the
$rootScope
and work your way down the scope hierarchy./**
* @name myApp.rootScope
* @property {Object} Math
*/
var app = angular.module('myApp',[]);
app.run(function(/** myApp.rootScope **/ $rootScope) {
$rootScope.Math = Math;
});
It doesn’t have to be fancy or commented. The goal is to define what properties are in the
$rootScope
, because these properties will exist on all other scopes. So when defining a new scope we’ll tell the IDE that it’s extended from myApp.rootScope
.
This works really well when you are using controllers on routes. The controller will often add properties to the
$scope
that will be inherited by child scopes for that view./**
* @name myApp.BookControllerScope
* @extends myApp.rootScope
* @property {string} title
* @property {number} book_id
*/
app.controller('BookController',function(/** myApp.BookControllerScope */$scope, $routeParams) {
$scope.title = 'ThinkingMedia';
$scope.book_id = $routeParams.bookId;
});
app.config(function($routeProvider) {
$routeProvider.when('/Book/:bookId', {
templateUrl: 'book.html',
controller: 'BookController'
});
});
In the above example the
BookController
uses a $scope
defined with jsDoc. The properties title
and book_id
will have intellisense features from an IDE that supports jsDoc. You can now inherit from themyApp.BookControllerScope
scope when defining a new scope that will exist in that route’s view.
For example;
/**
* @name myApp.ReaderControllerScope
* @extends myApp.BookControllerScope
* @property {string} chapter
* @property {number} page
*/
app.controller('ReaderController',function(/** myApp.ReaderControllerScope */$scope) {
$scope.chapter = $scope.title + ' - Tutorials';
$scope.page = 1;
});
In the above example the IDE will provide auto-complete features to show that
title
is a property that exists already on the $scope
object. This kind of insight can be invaluable on larger AngularJS projects.
This article taken form here.
Angular 2 is an open source JavaScript framework to build web applications in HTML and JavaScript and has been conceived as a mobile first approach.
ReplyDeleteYou should have a basic understanding of JavaScript and any text editor. As we are going to develop web-based applications using Angular 2, it will be good if you have an understanding of other web technologies such as HTML, CSS, AJAX, AngularJS etc.
Read more: AngularJS Training and Tutorial