In the previous chapter, Basics, I asked you to rein in your JavaScript horses while we explored Angular's original value proposition, which is to provide the front-end developer with powerful extensions to HTML. Of course, the Angular story doesn't end there, and customizing behavior via JavaScript is the norm for virtually every Angular project. If you spent the first chapter wondering when we would get to some real coding, your patience will now be rewarded. It's time to write JavaScript.
The most common way to augment an Angular view using JavaScript is with a controller. The easiest way to write a controller is as a plain old constructor function. In order to understand exactly what is happening, let's start out with a very simple controller. This is not even hello world. It does absolutely nothing.
This is our controller.
function EmptyController() {
};
Rendering the scope properties prepared by a controller requires the Angular library file. The fragment below will be copied inside the head
element of each rendered example.
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.js"></script>
In the HTML document, we must configure our Angular application to load the app
module. In the previous chapter, Basics, I showed how to add the ng-app
directive to an element in order to tell Angular which part of your document it should process. Look at the usage of ng-app
in the example below. Can you pick out the new addition?
<body ng-app="app">
<!-- Other examples to be inserted here. -->
</body>
Controllers can be defined as constructor functions declared on the global scope, and that is the approach we will use in this chapter. This simple form of controller was supported by default in Angular versions up to 1.3 to facilitate developer onboarding, but it now requires the following configuration, which involves creating a named application module. The details of using angular.module
will be explained in the upcoming chapters Modules, Dependency Injection, and Services. For now, just treat the example below as some required boilerplate.
angular.module('app', []);
angular.module('app').config(['$controllerProvider', function($controllerProvider) {
$controllerProvider.allowGlobals();
}]);
Now that we have that out of the way, let's try out our noop controller.
As usual, the way to do it is with a directive. The ng-controller
directive finds and invokes the controller function passed to it by name.
<p ng-controller="EmptyController">
</p>
As you may have expected, invoking this empty controller function does nothing. Now then, what are these controllers? How do we use them?
The conceptual overview in the official guide states that the controller's job is to “expose variables and functionality to expressions and directives.” The “functionality” refers to callback functions. We will get to those in a little while. Right now, let's talk about initializing “variables”, or preparing the model.
Keeping in mind that the Angular model is just ordinary JavaScript that can be reached through an expression's scope, preparing the model is a piece of cake. Ordinary JavaScript? Sure, we can write ordinary JavaScript. Let's add a simple string property to our empty controller.
function MessageController() {
this.message = "This is a model.";
}
Simple, right? Are we done? Does the assignment above create a model? Yes? No?
The answer is almost. When we can reach the message
property through the view's scope, then it will be a model. One way to accomplish this is to expose the entire controller as a property on the scope. This may sound like a big deal, but if you know the secret syntax, it is easy to do.
As explained in the API documentation for ng-controller
, you can pass a Controller as propertyName
expression as the ng-controller
argument. Please note that you need Angular version 1.2 or later to use this feature.
<p ng-controller="MessageController as controller">
{{controller.message}}
</p>
Look at that, we have just prepared model data using a controller.
Although the method of attaching properties directly to the this
reference is straightforward, exposing the entire controller to the view gives the misleading impression that the controller is part of the model, when traditionally, the controller's role is just to prepare the model. Testing and debugging is often easier when you limit access to only what a client needs and nothing more. Also, this approach adds noise to the view code, since all properties must be accessed through the reference to the controller. View code is typically the most critical area of a web application for noise; it's the first place where we are likely to have trouble understanding intentions at a glance. Finally, controllers work hand-in-hand with the scope, so for instructional purposes it will be helpful to see a reference to the scope and have fine-grained control over it. With this control, we can explicitly expose only what we want to the view. Therefore, this book will prefer the controller style that explicitly manages the scope object.
The first question, then, is how we can get a reference to this scope object. Do we create it using the new
operator? Do we request it from somewhere?
No, we get it via dependency injection. You also may have noticed that I have not exhibited code that instantiates a controller, such as var controller = new MessageController();
. Who is creating the controllers used in the views, then?
Angular is, of course. Angular is an Inversion of Control container that manages the lifecycle of application components. When a new controller or other component is needed, Angular constructs one. This saves us work, but more importantly, it makes it possible for Angular to inject resources, or dependencies, into our components.
As a component that is managed by the dependency injection framework, our controller just needs a parameter named $scope
added to it. The naming of this parameter is critical. So critical, in fact, that if you minify your JavaScript source code as part of your build, you will break the dependency injection mechanism. (Don't worry, there is a workaround, explained in the Dependency Injection chapter.)
function MessageController($scope) {
$scope.message = "This is a model.";
}
By adding the $scope
parameter to our constructor function, we inform Angular that we want a reference to the view's scope. Really, could this be any easier? Now, instead of declaring the message
property on this
(the controller), we declare it directly on the scope.
In the template, we remove the as controller
from the ng-controller
directive. The expression controller.message
becomes just message
, which is now the only property that we have attached to the scope.
<p ng-controller="MessageController">
{{message}}
</p>
While both methods of exposing model data work, this book will mostly use the method that injects the explicit $scope
reference. Dependency injection is an integral part of using angular, as you shall see, so we may as well get some familiarity with it.
The definition of a controller from the official guide also mentions exposing “functionality” to the view. Before we move on to this area, I think it may be worth taking a moment to discuss how Angular controllers differ a bit from the classical MVC pattern. (Or, skip the academics.)
According to the Wikipedia entry for model-view-controller (MVC), a controller “mediates input, converting it to commands for the model or view.” However, it can be hard to understand the “commands for the model or view” part in the context of Angular, since Angular's version of the MVC model is radically simplified, with anemic models that as a rule contain no logic. In Angular, at least these days, the prevailing approach is to place all business logic, also known as domain logic, inside the controller. In other words, Angular applications trend toward skinny models and fat controllers.
If you are familiar with patterns such as rich domain model and Skinny Controller, Fat Model, I would expect you to find Angular's approach to be a step backward. I think it's probably just a question of priorities. In client-side programming today, the critical distinction is between declarative, DOM-oriented view code on one side, and imperative, data-driven JavaScript code handling business logic and application infrastructure on the other. That's a big win, and I'm happy that frameworks such as Angular are focused on it. Someday perhaps, separating business logic from the other responsibilities of controllers will become more of a priority, and we will see a trend toward richer models.
Let's write a controller that exposes a very basic function, simply to test that we can somehow invoke this function from the view. As you may recall from the previous chapter, Angular did not permit us to declare and expose functions within the view.
The controller below assigns a basic function to a property on the scope.
function CountController($scope) {
$scope.count = function() { return 12; }
}
Invoking the function within an expression is similar to normal JavaScript: We just use parentheses.
<p ng-controller="CountController">
There are {{count()}} months in a year.
</p>
Note that the function is not simply invoked and forgotten, but is in fact bound to the model. What does that mean? Well, Angular's binding infrastructure is something that you have probably begun to take for granted, but again, it means that Angular will invoke the function not only when the view is first rendered, but anytime the associated model is changed. In order to see this at work, we need to write a function that uses a model property. The add
function below uses two model properties that are declared on the scope, operand1
and operand2
. Angular will invoke the function and render the result whenever either or both properties are changed.
function AdditionController($scope) {
$scope.operand1 = 0;
$scope.operand2 = 0;
$scope.add = function() {
return $scope.operand1 + $scope.operand2;
}
$scope.options = [0,1,2,3,4];
}
Since we've already covered several examples of the input
directive in the previous chapter, let's instead use Angular's select
directive to change the model values. You may have already noticed that the last line in the controller prepares an options
model, which we will use in a comprehension inside this directive's ng-options
parameter. The x for x
of our comprehension may seem redundant, since it just returns the original list of values, but the directive requires us to write it nonetheless. (When your model is an array of objects, which is more typical, you might write a comprehension that picks out a name
property for the option label, using x.name for x
. Refer to the select
directive's parameters for more detail.)
<p ng-controller="AdditionController">
<select ng-model="operand1" ng-options="x for x in options"></select>
+ <select ng-model="operand2" ng-options="x for x in options"></select>
= {{add()}}
</p>
Ok, this works great. However, the design of the add
function can be improved by generalization, which will come in handy if we ever want to use it with properties other than operand1
and operand2
. Break out your refactoring manual and roll up your sleeves for some parameter extraction.
function AdditionController($scope) {
$scope.number = 2;
$scope.add = function(operand1, operand2) {
return operand1 + operand2;
}
}
Within expressions, you can pass both properties and literals to functions, just like in regular JavaScript.
<p ng-controller="AdditionController">
{{add(number, 2)}} is not the same as {{add(number, "2")}}
<br>
2 + 2 + 2 + 2 = {{add(2, add(2, add(2, 2)))}}
</p>
Now, let's expose a callback function that handles a user action.
There is an example in the previous chapter demonstrating how an expression passed to ng-click
can toggle a boolean model property. It initializes the model, authorized
, within an ng-init
directive, then manipulates it using a simple, inline callback: ng-click="authorized = !authorized"
. Let's adapt the example by moving the model initialization and toggle logic to its proper home, a controller.
function AuthController($scope) {
$scope.authorized = true;
$scope.toggle = function() {
$scope.authorized = !$scope.authorized
};
}
Now that toggle
is a function available on the scope, the argument to ng-click
looks like a function invocation: toggle()
. It isn't really an immediate invocation, of course. It's just a string that will be evaluated later, when the user clicks.
<div ng-controller="AuthController">
<p>
The secret code is
<span ng-show="authorized">0123</span>
<span ng-hide="authorized">not for you to see</span>
</p>
<input class="btn btn-xs btn-default" type="button" value="toggle" ng-click="toggle()">
</div>
The example still works, and our simple toggle logic is now in a better place. Why better? Good question. Books usually explain techniques for managing complexity using examples that are much too simple to show the motivation or benefit. This book is no different. In the last chapter, I requested that you suspend judgement about using expressions in your HTML. So, if you really embraced Angular's approach toward enhancing HTML with dynamic behavior, you may be wondering now what justifies the extra complexity of controllers.
Easier testing is one of the classic reasons to move code from a complex context (templates) to a simpler one (controllers). Also, you just can't fit much within a template. The benefit of handling user actions inside the controller becomes clearer when we code more complex behavior, such as synchronizing model data with a remote server. (If you're impatient to see how this works, you can jump ahead to the HTTP chapter.)
In this chapter, we introduced JavaScript into our usage of Angular, in the form of controllers, which in an MVC pattern have the responsibility of preparing data for our views. Controllers make data available to views by declaring properties on a scope object. In the next chapter, we'll take a closer look at scope objects, and learn how they are organized as a hierarchy that roughly follows the structure of the DOM fragment managed by our application.