Bidirectional Communication Between Directives and Controllers in Angular

In Angular, it’s very easy for a directive to call into a controller. Working in the other direction – that is, calling a directive function from the controller – is not quite as intuitive. In this blog post, I’ll show you an easy way for your controllers to call functions defined in your directives in your Angular applications.

Like I said, calling a controller function from a directive is straightforward. You simply define a “callback” function in the controller and pass it to the directive (using the ‘&’ symbol in the isolated scope definition). It’s then trivial for the directive to invoke the function, which calls into the controller. To put things in .NET terms, this is akin to a user control (the directive) raising an event, which the user control’s host (the controller) can handle.

For example, you may want your directive to call your controller when the user clicks a button defined inside the directive’s template:

myPage.html

<div ng-controller=”myController”>
    <my-directive on-button-click=”vm.directiveButtonClicked()” />
</div>

myController.js

function myController($scope) {
    var vm = this;
    vm.directiveButtonClicked = function () {
        // Controller reacting to call initiated by directive
        alert(‘Button was clicked in directive’);
    }
}

myDirectiveTemplate.html

<button ng-click=”buttonClicked”>Click Me</button>

myDirective.js

function myDirective() {
    return {
        restrict: ‘E’,
        templateUrl: ‘/Templates/myDirectiveTemplate.html’,
        scope: {
            onButtonClick: ‘&’
        },
        link: link
    };

    function link(scope, element, attrs, controller) {
        scope.buttonClicked = function () {
            // Button was clicked in the directive
            // Invoke callback function on the controller
            scope.onButtonClick();
         }
     }
}

Unfortunately, there is no clearly established pattern in Angular for communicating in the opposite direction (calling functions of the directive from the controller). Again, in .NET terms, it’s easy for a user control’s host (the controller) to invoke public or internal methods defined by the user control (the directive). But there is no native way to achieve the same thing in Angular, which is certainly curious, because this is not an uncommon requirement.

Several solutions to this problem can be found on the web, but most of them carry caveats and/or add unwanted complexity. Some work by using $watch, but $watch is undesirable and should generally be avoided when possible. Others work, but not with isolated scope, which means you won’t achieve isolation across multiple instances of the directive.

Fret not! I’m going to show you a simple, lightweight technique that will enable your controllers to call functions on your directives, without resorting to $watch, and with full support for isolated scope.

Here’s how it works:

  1. The controller defines a view-model object named “accessor” with no members
  2. The page passes this object to the directive, via an attribute also named “accessor”
  3. The directive receives the accessor, and attaches a function to it
  4. The controller is now able to call the directive function via the accessor

Let’s demonstrate with an example. The directive template has two text boxes for input, but no button. Instead, there is a button on the page that is wired to a handler on the page’s controller. When the user clicks the button, the controller calls the directive. In response, the directive prepares an object with data entered by the user in the text boxes and returns it to the controller.

myPage.html

<div ng-controller=”myController”>
    <my-directive accessor=”vm.accessor” />
    <button ng-click=”vm.callDirective()”>Get Data</button>
</div>

myController.js

function myController($scope) {
    var vm = this;
    vm.accessor = {};
    vm.callDirective = function () {
        if (vm.accessor.getData) {
            var data = vm.accessor.getData();
            alert(‘Data from directive: ‘ + JSON.stringify(data));
        }
    };
}

myDirectiveTemplate.html

Name: <input type=”text” ng-model=”name” /><br />
Credit: <input type=”text” ng-model=”credit” /><br />

myDirective.js

function myDirective() {
    return {
        restrict: ‘E’,
        templateUrl: ‘/Templates/myDirectiveTemplate.html’,
        scope: {
            accessor: ‘=’
        },
        link: link
    };

    function link(scope, element, attrs, controller) {
        if (scope.accessor) {
            scope.accessor.getData = function () {
                return {
                    name: scope.name,
                    credit: scope.credit
                };
            };
        }
    }
}

Notice how the controller defines vm.accessor as a new object with no members. The controller’s expectation is that the directive will attach a getData function to this object. And the directive’s expectation is that the controller has defined and passed in the accessor object specifically for this purpose. Defensive coding patterns are employed on behalf of both expectations; that is, we ensure that no runtime error is raised by the browser in case the controller doesn’t define and pass in the expected accessor object, or in case the directive doesn’t attach the expected function to the accessor object.

The accessor pattern described in this blog post simplifies the task of bi-directional communication, making it just as easy to call your directive from your controller as it is to call in the other direction.

Happy coding!