CHAPTER 2
Controllers in Angular provide the business logic to handle view behavior; for example, responding to a user clicking a button or entering some text in a form. Additionally, controllers prepare the model for the view template.
As a general rule, a controller should not reference or manipulate the Document Object Model (DOM) directly. This has the benefit of simplifying unit testing controllers.
You wish to assign a default value to the scope in the controller's context.
Use the ng-controller directive in your template:
<div ng-controller="MyCtrl"> |
Next, define the scope variable in your controller function:
var MyCtrl = function($scope) { |
You can find the complete example on GitHub.
Depending on where you use the ng-controller directive, you define its assigned scope. The scope is hierarchical and follows the DOM node hierarchy. In our example, the value expression is correctly evaluated to some value, since value is set in the MyCtrl controller. Note that this would not work if the value expression were moved outside the controller’s scope:
<p>{{value}}</p> |
In this case {{value}} will simply not be rendered at all due to the fact that expression evaluation in Angular.js is forgiving for undefined and null values.
You wish to increment a model value by 1 using a controller function.
Implement an increment function that changes the scope:
function MyCtrl($scope) { |
This function can be directly called in an expression; in our example we use ng-init:
<div ng-controller="MyCtrl"> |
You can find the complete example on GitHub.
The ng-init directive is executed on page load and calls the function incrementValue defined in MyCtrl. Functions are defined on the scope very similarly to values but must be called with the familiar parenthesis syntax.
Of course, it would have been possible to increment the value right inside of the expression with value = value +1 but imagine the function being much more complex! Moving this function into a controller separates our business logic from our declarative view template, and we can easily write unit tests for it.
You wish to retrieve a model via a function (instead of directly accessing the scope from the template) that encapsulates the model value.
Define a getter function that returns the model value:
function MyCtrl($scope) { |
Then, in the template, we use an expression to call it:
<div ng-controller="MyCtrl"> |
You can find the complete example on GitHub.
MyCtrl defines the getIncrementedValue function, which uses the current value and returns it incremented by 1. One could argue that, depending on the use case, it would make more sense to use a filter. But there are use cases specific to the controller’s behavior where a generic filter is not required.
You wish to react on a model change to trigger some further actions. In our example, we simply want to set another model value depending on the value we are listening to.
Use the $watch function in your controller:
function MyCtrl($scope) { |
In our example, we use the text input value to print a friendly greeting:
<div ng-controller="MyCtrl"> |
The value greeting will be changed whenever there's a change to the name model and the value is not blank.
You can find the complete example on GitHub.
The first argument name of the $watch function is actually an Angular expression, so you can use more complex expressions (for example: [value1, value2] | json) or even a JavaScript function. In this case, you need to return a String in the watcher function:
$scope.$watch(function() { |
The second argument is a function that is called whenever the expression evaluation returns a different value. The first parameter is the new value and the second parameter is the old value. Internally, this uses angular.equals to determine equality, which means both objects or values pass the === comparison.
You wish to share a model between a nested hierarchy of controllers.
Use JavaScript objects instead of primitives or direct $parent scope references.
Our example template uses a controller MyCtrl and a nested controller MyNestedCtrl:
<body ng-app="MyApp"> |
The app.js file contains the controller definition and initializes the scope with some defaults:
var app = angular.module("MyApp", []); |
Play around with the various input fields and see how changes affect each other.
You can find the complete example on GitHub.
All the default values are defined in MyCtrl, which is the parent of MyNestedCtrl. When making changes in the first input field, the changes will be in sync with the other input fields bound to the name variable. They all share the same scope variable as long as they only read from the variable. If you change the nested value, a copy in the scope of the MyNestedCtrl will be created. From now on, changing the first input field will only change the nested input field, which explicitly references the parent scope via $parent.name expression.
The object-based value behaves differently in this regard. Whether you change the nested or the MyCtrl scope’s input fields, the changes will stay in sync. In Angular, a scope prototypically inherits properties from a parent scope. Objects are, therefore, references and kept in sync, whereas primitive types are only in sync as long they are not changed in the child scope.
Generally, I tend to not use $parent.name and instead always use objects to share model properties. If you use $parent.name, the MyNestedCtrl not only requires certain model attributes but also a correct scope hierarchy with which to work.
Tip: The Chrome plug-in Batarang simplifies debugging the scope hierarchy by showing you a tree of the nested scopes. It is awesome!
You wish to share business logic between controllers.
Utilize a Service to implement your business logic, and use dependency injection to use this service in your controllers.
The template shows access to a list of users from two controllers:
<div ng-controller="MyCtrl"> |
The service and controller implementation in app.js implements a user service and the controllers set the scope initially:
var app = angular.module("MyApp", []); |
You can find the complete example on GitHub.
The factory method creates a singleton UserService that returns two functions for retrieving all users and the first user only. The controllers get the UserService injected by adding it to the controller function as params.
Using dependency injection here is quite nice for testing your controllers since you can easily inject a UserService stub. The only downside is that you can't minify the code from above without breaking it, since the injection mechanism relies on the exact string representation of UserService. It is, therefore, recommended to define dependencies using inline annotations, which keep working even when minified:
app.controller("AnotherCtrl", ["$scope", "UserService", |
The syntax looks a bit funny but, since strings in arrays are not changed during the minification process, it solves our problem. Note that you could change the parameter names of the function since the injection mechanism relies on the order of the array definition only.
Another way to achieve the same is using the $inject annotation:
var anotherCtrl = function($scope, UserService) { |
This requires you to use a temporary variable to call the $inject service. Again, you could change the function parameter names. You will most likely see both versions applied in apps using Angular.
You wish to unit test your business logic.
Implement a unit test using Jasmine and the angular-seed project. Following our previous $watch recipe, this is how our spec would look:
describe('MyCtrl', function(){ |
You can find the complete example on GitHub.
Jasmine specs use describe and it functions to group specs and beforeEach and afterEach to set up and tear down code. The actual expectation compares the greeting from the scope with our expectation Greetings Frederik.
The scope and controller initialization is a bit more involved. We use inject to initialize the scope and controller as close as possible to how our code would behave at run time, too. We can't just initialize the scope as a JavaScript object {} since we would then not be able to call $watch on it. Instead, $rootScope.$new() will do the trick. Note that the $controller service requires MyCtrl to be available and uses an object notation to pass in dependencies.
The $digest call is required in order to trigger a watch execution after we have changed the scope. We need to call $digest manually in our spec whereas, at run time, Angular will do this for us automatically.