left-icon

AngularJS Succinctly®
by Frederik Dietz

Previous
Chapter

of
A
A
A

CHAPTER 2

Controllers

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 Document Object Model (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:

<div ng-controller="MyCtrl">
  <p>{{value}}</p>
</div>

Next, define the scope variable in your controller function:

var MyCtrl = function($scope) {
  $scope.value = "some value";
};

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 controller’s scope:

<p>{{value}}</p>

<div ng-controller="MyCtrl">
</div>

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.

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:

function MyCtrl($scope) {
  $scope.value = 1;

  $scope.incrementValue = function(increment) {
    $scope.value += increment;
  };
}

This function can be directly called in an expression; in our example we use ng-init:

<div ng-controller="MyCtrl">
  <p ng-init="incrementValue(1)">{{value}}</p>
</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 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.

Encapsulating a Model Value with a Controller Function

Problem

You wish to retrieve a model via a 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:

function MyCtrl($scope) {
  $scope.value = 1;

  $scope.getIncrementedValue = function() {
    return $scope.value + 1;
  };
}

Then, in the template, we use an expression to call it:

<div ng-controller="MyCtrl">
  <p>{{getIncrementedValue()}}</p>
</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 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.

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:

function MyCtrl($scope) {
  $scope.name = "";

  $scope.$watch("name", function(newValue, oldValue) {
    if ($scope.name.length > 0) {
      $scope.greeting = "Greetings " + $scope.name;
    }
  });
}

In our example, we use the text input value to print a friendly greeting:

<div ng-controller="MyCtrl">
  <input type="text" ng-model="name" placeholder="Enter your name">
  <p>{{greeting}}</p>
</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:

$scope.$watch(function() {
  return $scope.name;
}, function(newValue, oldValue) {
  console.log("change detected: " + newValue)
});

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.

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:

<body ng-app="MyApp">
  <div ng-controller="MyCtrl">
    <label>Primitive</label>
    <input type="text" ng-model="name">

    <label>Object</label>
    <input type="text" ng-model="user.name">

    <div class="nested" ng-controller="MyNestedCtrl">
      <label>Primitive</label>
      <input type="text" ng-model="name">

      <label>Primitive with explicit $parent reference</label>
      <input type="text" ng-model="$parent.name">

      <label>Object</label>
      <input type="text" ng-model="user.name">
    </div>
  </div>
</body>

The app.js file contains the controller definition and initializes the scope with some defaults:

var app = angular.module("MyApp", []);

app.controller("MyCtrl", function($scope) {
  $scope.name = "Peter";
  $scope.user = {
    name: "Parker"
  };
});

app.controller("MyNestedCtrl", function($scope) {
});

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 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!

Sharing Code between Controllers using Services

Problem

You wish to share business logic between controllers.

Solution

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">
  <ul ng-repeat="user in users">
    <li>{{user}}</li>
  </ul>
  <div class="nested" ng-controller="AnotherCtrl">
    First user: {{firstUser}}
  </div>
</div>

The service and controller implementation in app.js implements a user service and the controllers set the scope initially:

var app = angular.module("MyApp", []);

app.factory("UserService", function() {
  var users = ["Peter", "Daniel", "Nina"];

  return {
    all: function() {
      return users;
    },
    first: function() {
      return users[0];
    }
  };
});

app.controller("MyCtrl", function($scope, UserService) {
  $scope.users = UserService.all();
});

app.controller("AnotherCtrl", function($scope, UserService) {
  $scope.firstUser = UserService.first();
});

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 keep working even when minified:

app.controller("AnotherCtrl", ["$scope", "UserService",
  function($scope, UserService) {
    $scope.firstUser = UserService.first();
  }
]);

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) {
  $scope.firstUser = UserService.first();
};

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 likely 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:

describe('MyCtrl', function(){
  var scope, ctrl;

  beforeEach(inject(function($controller, $rootScope) {
    scope = $rootScope.$new();
    ctrl = $controller(MyCtrl, { $scope: scope });
  }));

  it('should change greeting value if name value is changed', function() {
    scope.name = "Frederik";
    scope.$digest();
    expect(scope.greeting).toBe("Greetings Frederik");
  });
});

You can find the complete example on GitHub.

Discussion

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.

Scroll To Top
Disclaimer
DISCLAIMER: Web reader is currently in beta. Please report any issues through our support system. PDF and Kindle format files are also available for download.

Previous

Next



You are one step away from downloading ebooks from the Succinctly® series premier collection!
A confirmation has been sent to your email address. Please check and confirm your email subscription to complete the download.