English |  Español |  Français |  Italiano |  Português |  Русский |  Shqip

AngularJS Book

Chapter 5. The Controller in Detail

The controller is where your business logic lives.  To define business logic, let's say it's the layer between your UI and your data store, and any algorithms needed to support that. What a controller isn't, is a place to put DOM modification code. The controller doesn't know about the DOM, and should be decoupled from the view. Why is it then that other JavaScript frameworks want you to do exactly what I'm telling you not to do? I think the answer lies along the lines of "there's nowhere else to put it." With AngularJS, we DO have a place to put it, and not a controller (it's a directive, which we will explore later).

Adding Functionality to Our Controller

Now that we have a nice view, we need to add some functionality to our controller(s) to support our new markup.  The new stuff displayed a couple of blog posts and gave you a way to create a new one, among other things.  Let's see how that works behind-the-scenes:

// Header controller presents a description for the blog
blog.controller('HeadCtrl', function ($scope) {
$scope.description = 'My important Blog';
}); // Controls the blog content including all methods related to posts
blog.controller('ContentCtrl', function ($scope, Posts) {
// Retrieve the posts from our "server". If this succeeds, we'll
// put the posts into our $scope and do some post-processing.
Posts.getPosts().success(function (data) {
var posts;
$scope.posts = data.posts;
posts = $scope.posts; 
// Convert timestamps to JS timestamps (w/ millisecond precision)
var i = posts.length;
while (i--) {
posts[i].date = posts[i].date * 1000;
}
});
// This closes all editors if we happen to click the "new post" button
$scope.$watch('posts', function (new_val, old_val) {
var i;
if (new_val !== old_val) {
i = new_val.length;
while (i--) {
new_val[i].editing = false;
}
}
});

The controller is continued below, but I wanted to stop and point out that the code that's written above is executed as soon as the controller is instantiated--that is to say, the HTML that the controller is attached to is rendered. Every time this controller is instantiated it'll grab the posts and set up a "watch." What follows are simply functions to be called later, generally by the view.

Above, we've defined a new controller called HeadCtrl, which we referenced in the markup from the previous chapter.  We renamed MainCtrl to ContentCtrl, and added something called a watch  to the $scope object. Essentially, this observes the state of an AngularJS expression, and executes code when that state changes. Specifically, when we go to create a new post, we disable the "editing" state of all other post objects.

Let's continue looking at the Controllers:

    // Begins editing a post by making a temporary copy of the title and body,
// and setting the "editing" flag for that post to be true.
$scope.edit = function (post) {
post.temp = {
title: post.title,
body: post.body
};
post.editing = true;
};
// Saves a post by copying the contents of the temp object into the
// title and body. Updates the author and post date, and sets the editing
// flag to false, thereby closing the text editors. In addition we set the
// controller-wide "new_post" model to be false, so the "New Post" button appears.
$scope.save = function (post) {
post.title = post.temp.title;
post.body = post.temp.body;
delete post.temp;
post.date = new Date();
post.author = 'me'; // in lieu of an authentication system
post.editing = false;
$scope.posts.unshift(post); // new posts go at the top
delete $scope.new_post;
};
// Cancels a post edit. Does not copy temp data, and sets the editing flag
// to false. In addition we set the controller-wide "new_post" model to be
// false, so the "New Post" button appears.
$scope.cancel = function (post) {
delete post.temp;
post.editing = false;
delete $scope.new_post;
}; // Creates a new post
$scope.newPost = function () {
$scope.new_post = {
temp: {
title: 'Enter Title Here',
body: 'Enter Body Here'
},
editing: true
};
};
});

We've added an edit() function accessible to the view, which accepts a Post object and creates a temporary place for us to edit.  We made a save() function, which copies information from our temp object into its proper place and prepends it to the list of Posts.  We made a cancel() function, which aborts an edit.  Finally, we created a newPost() function that creates a temporary new Post object (note that at this time we don't actually have a Post pseudoclass; we'll set this up later).

As you can see above, these controllers basically do four things:

  • Put models on the $scope object
  • Put functions on the $scope object
  • Set up watches and events using $watch, $on, $broadcast, and $emit(we'll get to these last three later)
  • Execute "on load" functionality; when the view the controller is attached to is rendered by the browser

That's about all your controllers are ever going to do.

Relationship to the Model

The controller is a place to reference a model in the scope, but it is not the place to reference a model. A model can be kept in a service, factory, constant, value, or even a directive. However, most of the time you are going to be storing your model in a controller, since this is the first and most obvious place for it to go; on the controller's $scope object. Consider our example above: we have a model foo in the $scope object. The value of that model is a string.

It's not necessary to keep your model in a Scope. Mind you, if you don't, AngularJS will not watch the model for changes and will not update any views. If you don't need either of these things to happen, then you are better off not putting your model into a Scope, to save a bit of memory.

Relationship to the View

The controller typically sits between factories/services and the view. A view cannot access a service or factory directly; it must interact with a controller (or a directive). However, the controller knows squat about the view. Controllers are not meant to interact with the DOM. You can force this to happen if you absolutely must, but this is a deliberate design decision on the part of the AngularJS authors.  This makes controllers easily testable and potentially reusable.  If you have to switch back between testing logic and DOM manipulation in tests, it's going to be a mess.

Putting no DOM manipulation into a controller is likely in complete opposition to everything you have learned about MVC/MVVM/MVW. If you are coming from Backbone, another modern JavaScript framework, even it places the responsibility of template processing in the hands of a controller. AngularJS simply takes this schism a step further and asserts DOM manipulation is not business logic. It believes markup should be managed by markup, most likely in the case of a directive.

And a directive is where DOM manipulation should go, 9 times out of 10. There are cases where it's easier to put some functionality into a service, and we'll cover that later.

Relationship to Services and Factories

Common functionality shared between controllers is a great candidate for a service or factory. The controller can inject the necessary services, and that's that.

Dependency Injection

We touched on Dependency Injection earlier. Dependency injection, or DI for short, is a core concept of AngularJS. Certain components such as services, factories, constants, and values are all considered injectable. They are attached to the module and any service, controller, or various other components can request them. And yes, DI is magic. My advice is to stop worrying and simply be amazed.

I lied--it's not really magic. You can take a function and turn it into a string with the toString() method, available on all JavaScript objects. This is all AngularJS is doing; it converts functions to strings, runs string matching against them, and determines from the name of the parameter what you want to inject. Since JavaScript is pretty light on the introspection, this seems to be the most straightforward way to accomplish DI. The code may be a little icky, but what it buys for you is more than worth it.

What can you inject into a controller? You can inject services, factories, providers (configurable services; we'll cover this later), constants (using module.constant()), and values (using module.value()). Any of these that you have included, including those within submodules, will be available to every controller in your module. You will find that you can inject any of these constructs into services, factories, the module.run() command, and directives.

Creating a Controller

There are (at least) three different ways to create a controller. The first two use the controller() method of a Module object:

my_module.controller('MyCtrl', function($scope, SomeOtherService) {
...
}); my_module.controller('MyCtrl', ['$scope', 'SomeOtherService', function($scope, SomeOtherService) {
...
}]);

If you do not ever plan on compressing (not necessarily minifying by simply eliminating whitespace and comments; by "compressing" I mean strategies like renaming variables to shorter names) your JavaScript, the first syntax is perfectly acceptable. The DI matching regular expression will run against your function as expected. If you do plan on compressing your JavaScript, you should use the second syntax. Because the service/constant/whatever name is stored in a string, AngularJS will be able to ascertain what you meant by looking at the strings, even if the parameters in the function call become shortened.

The third syntax is even more straightforward:

function MyCtrl($scope, SomeOtherService) {
...
}

Yes, that's really all there is to it. The same conditions apply here, of course; if you plan on compressing your code this will not work. You must add the following after the function definition:

MyCtrl.$inject = ['$scope', 'SomeOtherService'];

This, like above, will store the parameter names as strings and make them available to AngularJS upon compression.

Why would you want to use one or the other? The first two syntaxes keep variables out of the local JavaScript scope. The third syntax pollutes the scope somewhat. It also makes it a bit more straightforward to accomplish controller inheritance.

Controller Inheritance

You won't need this. Most of what you want to do can be placed inside of a Scope, and its prototypical inheritance will serve you just fine. If you have common functionality shared between controllers that are, say, siblings, consider putting that functionality in a parent controller, or removing the functionality from the controllers altogether and using a service or factory.

Unit Testing

You can unit test controllers, and you should. It's very easy. Here's an example of testing our controller using Jasmine:

/*global beforeEach, describe, afterEach, module, expect, it, inject*/
(function () {
'use strict';
// grab the blog module before each test
beforeEach(module('blog'));
describe('Blog Controllers', function () {
var scope, // controllers need scopes, so here's ours
$controller; // $controller service to instantiate controllers

beforeEach(inject(function ($injector, $rootScope) {
scope = $rootScope.$new(); // make brand new child scope off of root
$controller = $injector.get('$controller');
}));
afterEach(function () {
scope.$destroy(); // eliminate our scope after each test run
});

describe('HeadCtrl', function () {
// all that HeadCtrl does is set a description. That's pretty easy to test.
it('should set the description', function () {
$controller('HeadCtrl', {$scope: scope});
expect(scope.description).toBe('My Important Blog');
});
});
});
})();

Again, to run this you will need a working Testacular install with proper configuration file. There is very little interesting about this configuration file, on the main, except the files array. Mine looks similar to this:

files = [
JASMINE,
JASMINE_ADAPTER,
'/path/to/blog/angular.min.js',
'/path/to/blog/angular-mocks.js',
'/path/to/blog/blog.js',
'/path/to/blog/blogSpec.js'
];

Note that we must explicitly include AngularJS and the mocks module. Next, we include the code to be tested, and finally the test file itself.

This file reads a little like a sentence, which should help you to understand what's going on even if you are completely new to unit testing. In a nutshell, the test (not the configuration file) declares, "The Blog Controllers' HeadCtrl should set the description to an explicit string." We need to do a little setup before each test we run in order to get the proper objects to test. Also, we do some cleanup afterwards. This is typical of constructing unit test fixtures. 

 

The assertion is made using AngularJS' $controller service. This service allows you to instantiate a controller. When unit testing, you're going to find that you often just call this service, pass it the proper parameters, then test the contents of the scope. You generally don't need to save a reference to the controller unless you are doing fancy things like controller inheritance, which I discouraged earlier.

Conclusion

Controllers are for your business logic.  Keeping your controllers tidy leads to dead-easy unit test writing.  Best of all, they are not necessarily dependent on any view, so you could reuse their functionality across your application.  A controller is also a common place to instantiate, declare or define models, since 99% of controllers will have a $scope object available to them.

In the next chapter we'll look at directives; truly the component that sets AngularJS apart from other frameworks.

There has been error in communication with Booktype server. Not sure right now where is the problem.

You should refresh this page.