Controllers
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 DOM directly. This has the benefit of simplifying unit testing controllers.
Assigning a Default Value to a Model
Problem
You wish to assign a default value to the scope in the controller’s context.
Solution
Use the ng-controller directive in your template:
1 <div ng-controller="MyCtrl">
2 <p>{{value}}</p>
3 </div>
Next, define the scope variable in your controller function:
1 var MyCtrl = function($scope) {
2 $scope.value = "some value";
3 };
You can find the complete example on github.
Discussion
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 controllers scope:
1 <p>{{value}}</p>
2
3 <div ng-controller="MyCtrl">
4 </div>
In this case {{value}} will simply be not rendered at all due to the fact that expression evaluation in Angular.js is forgiving for undefined and null values.
Changing a Model Value with a Controller Function
Problem
You wish to increment a model value by 1 using a controller function.
Solution
Implement an increment function that changes the scope.
1 function MyCtrl($scope) {
2 $scope.value = 1;
3
4 $scope.incrementValue = function(increment) {
5 $scope.value += increment;
6 };
7 }
This function can be directly called in an expression, in our example we use ng-init:
1 <div ng-controller="MyCtrl">
2 <p ng-init="incrementValue(1)">{{value}}</p>
3 </div>
You can find the complete example on github.
Discussion
The ng-init directive is executed on page load and calls the function incrementValue defined in MyCtrl. Functions are defined on the scope very similar 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.
Encapsulating a Model Value with a Controller Function
Problem
You wish to retrieve a model via function (instead of directly accessing the scope from the template) that encapsulates the model value.
Solution
Define a getter function that returns the model value.
1 function MyCtrl($scope) {
2 $scope.value = 1;
3
4 $scope.getIncrementedValue = function() {
5 return $scope.value + 1;
6 };
7 }
Then in the template we use an expression to call it:
1 <div ng-controller="MyCtrl">
2 <p>{{getIncrementedValue()}}</p>
3 </div>
You can find the complete example on github.
Discussion
MyCtrl defines the getIncrementedValue function, which uses the current value and returns it incremented by one. 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 controllers behavior where a generic filter is not required.
Responding to Scope Changes
Problem
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.
Solution
Use the $watch function in your controller.
1 function MyCtrl($scope) {
2 $scope.name = "";
3
4 $scope.$watch("name", function(newValue, oldValue) {
5 if ($scope.name.length > 0) {
6 $scope.greeting = "Greetings " + $scope.name;
7 }
8 });
9 }
In our example we use the text input value to print a friendly greeting.
1 <div ng-controller="MyCtrl">
2 <input type="text" ng-model="name" placeholder="Enter your name">
3 <p>{{greeting}}</p>
4 </div>
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.
Discussion
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:
1 $scope.$watch(function() {
2 return $scope.name;
3 }, function(newValue, oldValue) {
4 console.log("change detected: " + newValue)
5 });
The second argument is a function which gets called whenever the expression evaluation returns a different value. The first parameter is the new value and the second parameter the old value. Internally, this uses angular.equals to determine equality which means both objects or values pass the === comparison.
Sharing Models Between Nested Controllers
Problem
You wish to share a model between a nested hierarchy of controllers.
Solution
Use Javascript objects instead of primitives or direct $parent scope references.
Our example template uses a controller MyCtrl and a nested controller MyNestedCtrl:
1 <body ng-app="MyApp">
2 <div ng-controller="MyCtrl">
3 <label>Primitive</label>
4 <input type="text" ng-model="name">
5
6 <label>Object</label>
7 <input type="text" ng-model="user.name">
8
9 <div class="nested" ng-controller="MyNestedCtrl">
10 <label>Primitive</label>
11 <input type="text" ng-model="name">
12
13 <label>Primitive with explicit $parent reference</label>
14 <input type="text" ng-model="$parent.name">
15
16 <label>Object</label>
17 <input type="text" ng-model="user.name">
18 </div>
19 </div>
20 </body>
The app.js file contains the controller definition and initializes the scope with some defaults:
1 var app = angular.module("MyApp", []);
2
3 app.controller("MyCtrl", function($scope) {
4 $scope.name = "Peter";
5 $scope.user = {
6 name: "Parker"
7 };
8 });
9
10 app.controller("MyNestedCtrl", function($scope) {
11 });
Play around with the various input fields and see how changes affect each other.
You can find the complete example on github.
Discussion
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 scopes 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 to work with.
|
Tip: The Chrome plugin Batarang simplifies debugging the scope hierarchy by showing you a tree of the nested scopes. It is awesome! |
Sharing Code Between Controllers using Services
Problem
You wish to share business logic between controllers.
Solution
Utilise 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:
1 <div ng-controller="MyCtrl">
2 <ul ng-repeat="user in users">
3 <li>{{user}}</li>
4 </ul>
5 <div class="nested" ng-controller="AnotherCtrl">
6 First user: {{firstUser}}
7 </div>
8 </div>
The service and controller implementation in app.js implements a user service and the controllers set the scope initially:
1 var app = angular.module("MyApp", []);
2
3 app.factory("UserService", function() {
4 var users = ["Peter", "Daniel", "Nina"];
5
6 return {
7 all: function() {
8 return users;
9 },
10 first: function() {
11 return users[0];
12 }
13 };
14 });
15
16 app.controller("MyCtrl", function($scope, UserService) {
17 $scope.users = UserService.all();
18 });
19
20 app.controller("AnotherCtrl", function($scope, UserService) {
21 $scope.firstUser = UserService.first();
22 });
You can find the complete example on github.
Discussion
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 keeps working even when minified:
1 app.controller("AnotherCtrl", ["$scope", "UserService",
2 function($scope, UserService) {
3 $scope.firstUser = UserService.first();
4 }
5 ]);
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:
1 var anotherCtrl = function($scope, UserService) {
2 $scope.firstUser = UserService.first();
3 };
4
5 anotherCtrl.$inject = ["$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 probably see both versions applied in apps using Angular.
Testing Controllers
Problem
You wish to unit test your business logic.
Solution
Implement a unit test using Jasmine and the angular-seed project. Following our previous $watch recipe, this is how our spec would look.
1 describe('MyCtrl', function(){
2 var scope, ctrl;
3
4 beforeEach(inject(function($controller, $rootScope) {
5 scope = $rootScope.$new();
6 ctrl = $controller(MyCtrl, { $scope: scope });
7 }));
8
9 it('should change greeting value if name value is changed', function() {
10 scope.name = "Frederik";
11 scope.$digest();
12 expect(scope.greeting).toBe("Greetings Frederik");
13 });
14 });
You can find the complete example on github.
Discussion
Jasmine specs use describe and it functions to group specs and beforeEach and afterEach to setup and teardown 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 closely as possible to how our code would behave at runtime 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 runtime Angular will do this for us automatically.