AngularJS controller inheritance

The ng-controller directive can nested in HTML elements to create an effect known as controller inheritance. This is a feature that aims to reduce code duplication by letting you define common functionality in a parent controller and use it in one or more child controllers.

Check out this piece of code:
Index.html

<!DOCTYPE html>
<html ng-app="exampleApp">

  <head>
    <title>Controllers</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script src="controllers.js"></script>
    <link href="bootstrap.css" rel="stylesheet" />
    <link href="bootstrap-theme.css" rel="stylesheet" />
  </head>

 <body ng-controller="topLevelCtrl">

    <div class="well">
        <h4>Top Level Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
            </span>
            <input class="form-control" ng-model="dataValue">
        </div>
    </div>

    <div class="well" ng-controller="firstChildCtrl">
        <h4>First Child Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
            </span>
            <input class="form-control" ng-model="dataValue">
        </div>
    </div>    

    <div class="well" ng-controller="secondChildCtrl">
        <h4>Second Child Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
                <button class="btn btn-default" type="button"
                        ng-click="shiftFour()">Shift</button>
            </span>
            <input class="form-control" ng-model="dataValue">
        </div>
    </div>
</body>
</html>

You can check this code out via Plunker.

Controllers.js

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

app.controller("topLevelCtrl", function ($scope) {

    $scope.dataValue = "Hello, Alex";

    $scope.reverseText = function () {
        $scope.dataValue = $scope.dataValue.split("").reverse().join("");
    }

    $scope.changeCase = function () {
        var result = [];
        angular.forEach($scope.dataValue.split(""), function (char, index) {
            result.push(index % 2 == 1
                ? char.toString().toUpperCase() : char.toString().toLowerCase());
        });
        $scope.dataValue = result.join("");
    };
});

app.controller("firstChildCtrl", function ($scope) {

    $scope.changeCase = function () {
       $scope.dataValue = $scope.dataValue.toUpperCase();
    };
});

app.controller("secondChildCtrl", function ($scope) {

    $scope.changeCase = function () {
       $scope.dataValue = $scope.dataValue.toLowerCase();
    };

    $scope.shiftFour = function () {
        var result = [];
        angular.forEach($scope.dataValue.split(""), function (char, index) {
            result.push(index < 4 ? char.toUpperCase() : char);
        });
        $scope.dataValue = result.join("");
    }
});

There are three controllers at work in this example, each of which has been applied to a region of markup using the ng-controller directive. The controller called topLevelCtrl is applied to the body element, and two child controllers, firstChildCtrl and secondChildCtrl, are nested within it. In addition to the child controllers, the top-level controller contains its own elements, and all three controllers present an input element with some inline buttons that invoke controller behaviors.

When you nest controllers through the ng-controller directive, the scopes of the child controllers inherit the data and behaviors of the parent controller scope. Each controller in this example has its own scope, but the scopes for the child controllers contain the data values and behaviors of the parent controller.

You can see how this works you click the Reverse button. The input elements are all wired up to manage the dataValue property, and the Reverse buttons all call the reverseText behavior, both of which are defined by the top-level controller. The child controllers inherit the data value and the behavior, which is why all of the input elements change when you click any of the Reverse buttons, even those implemented by the child controllers.

Adding to the Inherited Data and Behaviors

The main benefit of using controller inheritance is the ability to mix functionality that is inherited from the parent scope with locally defined additions. You can see an example of this in the secondChildCtrl controller, which defines a behavior called shiftFour that makes the first four characters of the dataValue property uppercase, as follows:

$scope.shiftFour = function () {
    var result = [];
    angular.forEach($scope.dataValue.split(""), function (char, index) {
        result.push(index < 4 ? char.toUpperCase() : char);
    });
    $scope.dataValue = result.join("");
}

This behavior is available only in the scope of the secondChildCtrl controller—but notice that even here, I am able to use an inherited feature as I perform my changes on the dataValue property defined by the parent scope. You use this feature to build on the functionality of existing controllers, without having to duplicate behaviors and data.

Data inheritance

There are some trap in the previous example that affects just about everyone who uses controller inheritance for the first time. To see the problem click each of the Reverse buttons in turn (it doesn’t matter which order you click them in).

turn (it doesn’t matter which order you click them in).
This is the behavior that you would expect given my description so far. The Reverse button invokes the reverseText behavior, which operates on the dataValue property. The behavior and data are defined by the parent controller and inherited by the children, which is why the contents of all three input elements change together.

Now change the contents of the input element associated with the second child controller. It doesn’t matter what you enter, just as long as you change the text. Now click all three Reverse buttons in turn again, and you’ll see a different behavior. All three buttons operate on the first two input elements, and the input element that you edited remains unchanged. To dig deeper into the problem, click the Case and Shift buttons for the second child controller; these do change the final input element.

Here is the solution for this problem:
Index.html

<!DOCTYPE html>
<html ng-app="exampleApp">

  <head>
    <title>Controllers</title>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.3.15/angular.min.js"></script>
    <script src="controllers.js"></script>
    <link href="bootstrap.css" rel="stylesheet" />
    <link href="bootstrap-theme.css" rel="stylesheet" />
  </head>

 <body ng-controller="topLevelCtrl">

    <div class="well">
        <h4>Top Level Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
            </span>
            <input class="form-control" ng-model="data.dataValue">
        </div>
    </div>

    <div class="well" ng-controller="firstChildCtrl">
        <h4>First Child Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
            </span>
            <input class="form-control" ng-model="data.dataValue">
        </div>
    </div>    

    <div class="well" ng-controller="secondChildCtrl">
        <h4>Second Child Controller</h4>
        <div class="input-group">
            <span class="input-group-btn">
                <button class="btn btn-default" type="button"
                        ng-click="reverseText()">Reverse</button>
                <button class="btn btn-default" type="button"
                        ng-click="changeCase()">Case</button>
                <button class="btn btn-default" type="button"
                        ng-click="shiftFour()">Shift</button>
            </span>
            <input class="form-control" ng-model="data.dataValue">
        </div>
    </div>
</body>
</html>

Controllers.js

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

app.controller("topLevelCtrl", function ($scope) {

    $scope.data = {
        dataValue: "Hello, Alex"
    }

    $scope.reverseText = function () {
        $scope.data.dataValue = $scope.data.dataValue.split("").reverse().join("");
    }

    $scope.changeCase = function () {
        var result = [];
        angular.forEach($scope.data.dataValue.split(""), function (char, index) {
            result.push(index % 2 == 1
                ? char.toString().toUpperCase() : char.toString().toLowerCase());
        });
        $scope.data.dataValue = result.join("");
    };
});

app.controller("firstChildCtrl", function ($scope) {

    $scope.changeCase = function () {
       $scope.data.dataValue = $scope.data.dataValue.toUpperCase();
    };
});

app.controller("secondChildCtrl", function ($scope) {

    $scope.changeCase = function () {
       $scope.data.dataValue = $scope.data.dataValue.toLowerCase();
    };

    $scope.shiftFour = function () {
        var result = [];
        angular.forEach($scope.data.dataValue.split(""), function (char, index) {
            result.push(index < 4 ? char.toUpperCase() : char);
        });
        $scope.data.dataValue = result.join("");
    }
});

Code in Plunker: http://plnkr.co/edit/v1ZW10U5t7qUTQ84BJ32?p=info

Instead of defining dataValue as a property directly on the scope of the parent controller, I have defined it as a property of an object called data. The other changes in this file update the reference to the dataValue property to access it via the data object. You can see how I have reflected this change in the ng-model directives that link the input elements with the dataValue property in the controllers.html file.

If you load new version of this code, you will see that all of the buttons affect the contents of the input elements and that editing the input element content doesn’t stop subsequent changes from taking effect.

To understand what’s happening, we need to look at the way that AngularJS deals with inheritances of data values in scopes and how this is affected by the ng-model directive.

When you read the value of a property that is defined directly on the scope, AngularJS checks to see whether there is a local property in the controller’s scope and, if not, starts working its way up the scope hierarchy to see whether it has inherited one. However, when you use the ng-model directive to modify such a property, AngularJS checks to see whether the scope has a property of the right name and, if not, assumes you want to implicitly define it.

The effect is to override the property value. The reason that editing the contents of a child input element prevents the Reverse button from working is that there are now two dataValue properties—one defined by the top-level controller and one by the child you edited. The reverseText behavior is defined by the top-level controller, and it operates on the dataValue defined in the top-level scope, leaving the child’s dataValue property unaltered.

This doesn’t happen when you assign an object to the scope and then define your data properties on that object. This is because JavaScript implements what is known as prototype inheritance. What is important is the knowledge that defining properties directly on the scope like this:

$scope.dataValue = "Hello, Alex";

means that using the ng-model directive will create local variables, while using an object as an intermediary, like this:

$scope.data = {
    dataValue: "Hello, Alex"
}

ensures that ng-model will update the data values defined in the parent scope. This is not a bug. It is a deliberate feature that allows you to decide how your controller and its scope will work, and you can mix and match both techniques in the same scope. If you want a value that is initially shared but will be copied when modified, then define your data properties directly on the scope. To ensure that there is only one value, then define your data properties via an object.

The following example I took from the nice book called Pro AngularJS(Expert’s Voice in Web Development) by Adam Freeman. I recommend to read this book to everyone who wants to learn AngularJS.

Share this post:Tweet about this on TwitterShare on Facebook0Share on LinkedIn0Share on Google+0Share on Reddit0Email this to someoneDigg this