In this Lesson, we will cover the following recipes:
$apply
In this Lesson, you will discover strategies to keep your application clean—visually, structurally, and organizationally.
When initializing an AngularJS application, very frequently you will allow the framework to do it transparently with the ng-app
directive. When attached to a DOM node, the application will be automatically initialized upon the DOMContentLoaded
event, or when the framework script is evaluated and the document.readyState === 'complete '
statement becomes true. The application parses the DOM for the ng-app
directive, which becomes the root element of the application. It will then begin initializing itself and compiling the application template. However, in some scenarios, you will want more control over when this initialization occurs, and AngularJS provides you with the ability to do this with angular.bootstrap()
. Some examples of this include the following:
When manually bootstrapping, the application will no longer use the ng-app
directive. Suppose that this is your application template:
(index.html) <!doctype html> <html> <body> <div ng-controller="Ctrl"> {{ mydata }} </div> <script src="angular.js"></script> <script src="app.js"></script> </body> </html> (app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.mydata = 'Some scope data'; });
The AngularJS initialization needs to be triggered by an event after the angular.js
file is loaded, and it must be directed to a DOM element to be used as the root of the application. This can be accomplished in the following way:
(app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.mydata = 'Some scope data'; }); angular.element(document).ready(function() { angular.bootstrap(document, ['myApp']); });
JSFiddle: http://jsfiddle.net/msfrisbie/5nfgyxsz/
The angular.bootstrap()
method is used to link an existing application module to the designated DOM root node. In this example, the jqLite ready()
method is passed a callback, which indicates that the browser's document
object should be used as the root node of the myApp
application module. If you were to use ng-app
to auto-bootstrap, the following would roughly be the equivalent:
(index.html) <!doctype html> <html ng-app="myApp"> <body> <div ng-controller="Ctrl"> {{ mydata }} </div> <script src="angular.js"></script> <script src="app.js"></script> </body> </html>
By no means are you required to use the <html>
element as the root of your application. You can just as easily attach the application to an inner DOM element if your application only needed to manage a subset of the DOM. This can be done as follows:
(index.html) <!doctype html> <html ng-app="myApp"> <body> <div id="child"> <div ng-controller="Ctrl"> {{ mydata }} </div> </div> <script src="angular.js"></script> <script src="app.js"></script> </body> </html> (app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.mydata = 'Some scope data'; }); angular.element(document).ready(function() { angular.bootstrap(document.getElementById('child'), ['myApp']); });
JSFiddle: http://jsfiddle.net/msfrisbie/k4nn5Lha/
In the course of developing AngularJS applications, you will become very familiar with $apply()
and its implications. The $apply()
function cannot be invoked while the $apply()
phase is already in progress without causing AngularJS to raise an exception. While in simpler applications, this problem can be solved by being careful and methodical about where you invoke $apply()
; however, this becomes increasingly more difficult when applications incorporate third-party extensions with high DOM event density. The resulting problem is one where the necessity of invoking $apply
is indeterminate.
As it is entirely possible to ascertain the state of the application when $apply()
might need to be invoked, you can create a wrapper for $apply()
to ascertain the state of the application, and conditionally invoke $apply()
only when not in the $apply
phase, essentially creating an idempotent $apply()
method.
Suppose that this is your application:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> <button ng-click="increment()">Increment</button> {{ val }} </div> </div> (app.js) angular.module('myApp',[]) .controller('MainController', function($scope) { $scope.val = 0; $scope.increment = function() { $scope.val++; }; setInterval(function() { $scope.increment(); }, 1000); });
In this example, the use of setInterval()
means that a DOM event is occurring and AngularJS is not paying attention to it or what it does. The model is correctly being modified, but AngularJS's data binding is not propagating that change to the view. The button click, however, is using a directive that starts the $apply
phase. This would be fine; however, as it presently exists, clicking the button will update the DOM, but the setInterval()
callback will not.
Worse yet, incorporating a call to $scope.$apply()
inside the increment()
method does not solve the problem. This is because when the button is clicked, the method will attempt to invoke $apply()
while already in the $apply
phase, which as mentioned before, will cause an exception to be raised. The setInterval()
callback, however, will function properly.
The ideal solution is one where you are able to reuse the same method for both events, but $apply()
will be conditionally invoked only when it is needed. The most trivial and straightforward method of achieving this is to attach a safeApply()
method to the parent controller scope of the application and let inheritance propagate it throughout your application. This can be done as follows:
(app.js) angular.module('myApp', []) .controller('Ctrl', function ($scope) { $scope.safeApply = function (func) { var currentPhase = this.$root.$$phase; // determine if already in $apply/$digest phase if (currentPhase === '$apply' || currentPhase === '$digest') { // already inside $apply/$digest phase // if safeApply() was passed a function, invoke it if (typeof func === 'function') { func(); } } else { // not inside $apply/$digest phase, safe to invoke $apply this.$apply(func); } }; $scope.val = 0; // method that may or may not be called from somewhere // that will not trigger a $digest $scope.increment = function () { $scope.val++; $scope.safeApply(); }; // application component that modifies the model without // triggering a $digest setInterval(function () { $scope.increment(); }, 1000); });
JSFiddle: http://jsfiddle.net/msfrisbie/pnhmo2gx/
The current phase of the application can be determined by reading the $$phase
attribute of the root scope of the application. If it is either in the $apply
or $digest
phase, it should not invoke $apply()
. The reason for this is that $scope.$digest()
is the actual method that will check to see whether any binding values have changed, but this should only be called after the non-AngularJS events have occurred. The $scope.$apply()
method does this for you, and it will invoke $digest()
only after evaluating any function passed to it. Thus, inside the safeApply()
method, it should only invoke $apply()
if the application is not in either of these phases.
The preceding example will work fine as long as all scopes that want to use safeApply()
inherit from the controller scope on which it is defined. Even so, controllers are initialized relatively late in the application's bootstrap process, so safeApply()
cannot be invoked until this point. On top of this, defining something like safeApply()
inside a controller introduces a bit of code smell, as you would ideally like a method of this persuasion to be implicitly available throughout the entire application without relegating it to a specific controller.
A much more robust way of doing this is to decorate $rootScope
of the application with the method during the config
phase. This ensures that it will be available to any services, controllers, or directives that try to use it. This can be accomplished in the following fashion:
(app.js) angular.module('myApp', []) .config(function($provide) { // define decorator for $rootScope service return $provide.decorator('$rootScope', function($delegate) { // $delegate acts as the $rootScope instance $delegate.safeApply = function(func) { var currentPhase = $delegate.$$phase; // determine if already in $apply/$digest phase if (currentPhase === "$apply" || currentPhase === "$digest") { // already inside $apply/$digest phase // if safeApply() was passed a function, invoke it if (typeof func === 'function') { func(); } } else { // not inside $apply/$digest phase, // safe to invoke $apply $delegate.$apply(func); } }; return $delegate; }); }) .controller('Ctrl', function ($scope) { $scope.val = 0; // method that may or may not be called from somewhere // that will not trigger a $digest $scope.increment = function () { $scope.val++; $scope.safeApply(); }; // application component that modifies the model without // triggering a $digest setInterval(function () { $scope.increment(); }, 1000); });
JSFiddle: http://jsfiddle.net/msfrisbie/a0xcn9y4/
The AngularJS wiki notes that if your application needs to use a construct such as safeApply()
, then the location where you are invoking $scope.$apply()
isn't high enough in the call stack. This is true, and if you can avoid using safeApply()
, you should do so. That being said, it is easy to think up a number of scenarios similar to this recipe's example where using safeApply()
allows your code to remain DRY and concise, and for smaller applications, perhaps this is acceptable.
By the same token, the rigorous developer will not be satisfied with this and will desire an idiomatic solution to this problem aside from laborious code refactoring. One solution is to use $timeout
, as shown here:
(app.js) angular.module('myApp', []) .controller('Ctrl', function ($scope, $timeout) { $scope.val = 0; // method that may or may not be called from somewhere // that will not trigger a $digest $scope.increment = function () { // wraps model modification in $timeout promise $timeout(function() { $scope.val++; }); }; // application component that modifies the model without // triggering a $digest setInterval(function () { $scope.increment(); }, 1000); });
JSFiddle: http://jsfiddle.net/msfrisbie/sagmbkft/
The $timeout
wrapper is the AngularJS wrapper for window.setTimeout
. What this does is effectively schedule the model modification inside a promise that will be resolved as soon as possible and when $apply
can be invoked without consequence. In most cases, this solution is acceptable as long as the deferred $apply
phase does not affect other portions of the application.
Few things are less enjoyable than working on a project where the organization of the application files and modules is garbage, especially if the application is written by people other than you. Keeping your application file tree and module hierarchy clean and tidy will save you and whoever is reading and using your code lots of time in the long run.
Assume that an application you are working on is a generic e-commerce site, with many users who can view and purchase products, leave reviews, and so on.
There are several guidelines that can be followed to yield extremely tight and clean applications that are able to scale without bloating.
This might seem obvious, but the benefits of following the one module, one file, and one name approach are plentiful:
angular.module('my-module')
should only ever appear once. A file should not contain all or part of the two different modules.inventory-controller.js
./inventory/inventory-controller.js
should reflect its location in the hierarchy by being named something along the lines of inventory.controller
.Proper locality and organization of test files is not always obvious. Rigorously following this style guide is not mandatory, but choosing a unified naming and organization convention will save you a lot of headaches later on. This approach entails the following:
_test
to whatever module file it is testing. The inventory-controller.js
module will have its unit tests located in inventory-controller_test.js
.Applications that group by component type (all directives in one place and all controllers in another) will scale poorly. The file and module locality should reflect that which appears in AngularJS dependencies. This includes the following:
Some parts of your application will be used almost everywhere and some parts will only be used once. Your application structure should reflect this. This approach includes the following:
components/
directory. This directory can also hold common asset files and other shared application pieces.components/
directory if it makes sense to do so.With the tips mentioned in the preceding section, the e-commerce application will look something like this:
ng-commerce/ index.html app.js app-controller.js app-controller_test.js components/ login/ login.js login-controller.js login-controller_test.js login-directive.js login-directive_test.js login.css login.tpl.html search/ search.js search-directive.js search-directive_test.js search-filter.js search-filter_test.js search.css search.tpl.html shopping-cart/ checkout/ checkout.js checkout-conroller.js checkout-controller_test.js checkout-directive.js checkout-directive_test.js checkout.tpl.html checkout.css shopping-cart.js shopping-cart-controller.js shopping-cart-controller_test.js shopping-cart.tpl.html shopping-cart.css
The app.js
file is the top-level configuration file, complete with route definitions and initialization logic. JS files matching their directory names are the combinatorial files that bind all the directory modules together.
CSS files provide styling that is only used by that component in that directory. Templates also follow this convention.
As unique and elegant as AngularJS is, the reality of the situation is that it is a framework that lives inside asynchronously executed client-side code, and this requires some considerations. One of these considerations is the first-time delivery initialization latency. Especially when your application JS files are located at the end of the page, you might experience a phenomenon called "template flashing," where the uncompiled template is presented to the user before AngularJS bootstraps and compiles the page. This can be elegantly prevented using ng-cloak
.
Suppose that this is your application:
(index.html) <body> {{ youShouldntSeeThisBecauseItIsUndefined }} </body>
The solution is to simply declare sections of the DOM that the browser should treat as hidden until AngularJS tells it otherwise. This can be accomplished with the ng-cloak
directive, as follows:
(app.css) /* this css rule is provided in the angular.js file, but if AngularJS is not included in <head>, you must define this style yourself */ [ng:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; } (index.html) <body ng-cloak> {{ youShouldntSeeThisBecauseItIsUndefined }} </body>
JSFiddle: http://jsfiddle.net/msfrisbie/6tnxoozn/
Any section with ng-cloak
initially applied to it will be hidden by the browser. AngularJS will delete the ng-cloak
directive when it begins to compile the application template, so the page will only be revealed once compilation is complete, effectively shielding the user from the uncompiled template. In this case, as the entire <body>
element has the ng-cloak
directive, the user will be presented with a blank page until AngularJS is initialized and compiles the page.
It might not behoove you to cloak the entire application until it's ready. First, if you only need to compile a subset or subsets of a page, you should take advantage of that by compartmentalizing ng-cloak
to those sections. Often, it's better to present the user with something while the page is being assembled than with a blank screen. Second, breaking ng-cloak
apart into multiple locations will allow the page to progressively render each component it must compile. This will probably give the feeling of a faster load as you are presenting compiled pieces of the view as they become available instead of waiting for everything to be ready.
As is to be expected with a single-page application, you will be managing a large number of templates in your application. AngularJS has several template management solutions baked into it, which offer a range of ways for your application to handle template delivery.
Suppose you are using the following template in your application:
<div class="btn-group"> #{{ player.number }} {{ player.name }} </div>
The content of the template is unimportant; it is merely to demonstrate that this template has HTML and uncompiled AngularJS content inside it.
Additionally, assume you have the following directive that is trying to use the preceding template:
(app.js) angular.module('myApp', []) .directive('playerBox', function() { return { link: function(scope) { scope.player = { name: 'Jimmy Butler', number: 21 }; } }; });
The top-level template will look as follows:
(index.html) <div ng-app="myApp"> <player-box></player-box> </div>
There are four primary ways to provide the directive with the template's HTML. All of these will feed the template into $templateCache
, which is where the directive and other components tasked with locating a template will search first.
AngularJS is capable of generating a template from a string of uncompiled HTML. This can be accomplished as follows:
(app.js) angular.module('myApp', []) .directive('playerBox', function() { return { template: '<div>' + ' #{{ player.number }} {{ player.name }}' + '</div>', link: function(scope) { scope.player = { name: 'Jimmy Butler', number: 21 }; } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/8ct0u33z/
When the component cannot find a template in $templateCache
, it will make a request to the corresponding location on the server. This template will then receive an entry in $templateCache
, which can be used as follows:
(app.js)
angular.module('myApp', [])
.directive('playerBox', function() {
return {
// will attempt to acquire the template at this relative URL
templateUrl: '/static/js/templates/player-box.html',
link: function(scope) {
scope.player = {
name: 'Jimmy Butler',
number: 21
};
}
};
});
On the server, your file directory structure will look something like the following:
yourApp/ static/ js/ templates/ player-box.html
It is also possible to serve and register the templates along with another template. HTML inside <script>
tags with type="text/ng-template"
and the id
attribute set to the key for $templateCache
will be registered and available in your application. This can be done as follows:
(app.js) angular.module('myApp', []) .directive('playerBox', function() { return { templateUrl: 'player-box.html', link: function(scope) { scope.player = { name: 'Jimmy Butler', number: 21 }; } }; }); (index.html) <div ng-app="myApp"> <player-box></player-box> <script type="text/ng-template" id="player-box.html"> <div> #{{ player.number }} {{ player.name }} </div> </script> </div>
JSFiddle: http://jsfiddle.net/msfrisbie/kg95bn9g/
Even cleaner is the ability to directly insert your templates into $templateCache
on application startup. This can be done as follows:
(app.js) angular.module('myApp', []) .run(function($templateCache) { $templateCache.put( // the template key 'player-box.html', // the template markup '<div>' + ' #{{ player.number }} {{ player.name }}' + '</div>' ); }) .directive('playerBox', function() { return { templateUrl: 'player-box.html', link: function(scope) { scope.player = { name: 'Jimmy Butler', number: 21 }; } }; });
JSFiddle: http://jsfiddle.net/msfrisbie/mp79srjf/
All these denominations of template definitions are different flavors of the same thing: uncompiled templates are accumulated and served from within $templateCache
. The only real decision to be made is how you want it to affect your development flow and where you want to expose the latency.
Accessing the templates from a remote server ensures that you aren't delivering content to the user that they won't need, but when different pieces of the application are rendering, they will all need to generate requests for templates from the server. This can make your application sluggish at times. On the other hand, delivering all the templates with the initial application load can slow things down quite a bit, so it's important to make informed decisions on which part of your application flow is more latency-tolerant.
The last method of defining templates is provided in a popular Grunt extension, called grunt-angular-templates
. During the application build, this extension will automatically locate your templates and interpolate them into your index.html
file as JavaScript string templates, registering them in $templateCache
. Managing your application with build tools such as Grunt has huge and obvious benefits, and this recipe is no exception.
AngularJS 1.2 introduced the ability to namespace your controller methods using the "controller as" syntax. This allows you to abstract $scope
in controllers and provide more contextual information in the template.
Suppose you had a simple application set up as follows:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl"> {{ data }} </div> </div> (app.js) angular.module('myApp', []) .controller('Ctrl', function($scope) { $scope.data = "This is string data"; });
The simplest way to take advantage of the "controller as" syntax is inside the ng-controller
directive in a template. This allows you to namespace pieces of data in the view, which should feel good to you as more declarative views are the AngularJS way. The initial example can be refactored to appear as follows:
(index.html) <div ng-app="myApp"> <div ng-controller="Ctrl as MyCtrl"> {{ MyCtrl.data }} </div> </div> (app.js) angular.module('myApp', []) .controller('Ctrl', function() { this.data = "This is string data"; });
JSFiddle: http://jsfiddle.net/msfrisbie/yh3r2t6r/
Note that there is no longer a need to inject $scope
, as you are instead attaching the string attribute to the controller object.
This syntax can also be extended for use in directives. Suppose the application was retooled to exist as follows:
(index.html) <div ng-app="myApp"> <foo-directive></foo-directive> </div> (app.js) angular.module('myApp', []) .directive('fooDirective', function() { return { restrict: 'E', template: '<div>{{ data }}</div>', controller: function($scope) { $scope.data = 'This is controller scope data'; } }; });
This works, but the "controller as" syntactic sugar can be applied here to make the content of the directive template a little less ambiguous:
(app.js) angular.module('myApp', []) .directive('fooDirective', function() { return { restrict: 'E', template: '<div>{{ fooController.data }}</div>', controller: function() { this.data = 'This is controller data'; }, controllerAs: 'fooController' } });
JSFiddle: http://jsfiddle.net/msfrisbie/7uobd20v/
Using the "controller as" syntax allows you to directly reference the controller object within the template. By doing this, you are able to assign attributes to the controller object itself rather than to $scope
.
There are a couple of main benefits of using this style, which are as follows:
$scope
into controllers means that unit tests need some boilerplate initialization.44.220.184.63