How to expose behavior from a directive with isolated scope?

14,881

Solution 1

You can do this with an isolated scope by setting up a variable in the scope that's two-way bound to the controller (using '='). In your directive you can then assign the function to that variable, and angular will use the binding to find the corresponding variable in your controller. That variable will point to a function that your controller can call.

http://jsfiddle.net/GWCCr/

html: Note the new attrib:

<div ng-app="main">
    <div ng-controller="MyCtrl">
        <button ng-click="call()" >Call</button>
        <div id="container" my-directive my-fn="fnInCtrl"> </div>
    </div>
</div>

js:

angular.module("main", []).controller("MyCtrl", function($scope) {
    $scope.call = function() {
        $scope.fnInCtrl();
    };
}).directive("myDirective", function() {
    return {
        scope: {
            myFn: '='
        },
        controller: function($scope) {
            $scope.myFn = function() {
                console.log("myfn called");
            }
        }
    };
});

Solution 2

Rather than trying to figure out how to call a function hidden inside a directive, I think you should be asking yourself: why do I want to call a function defined in a directive?

One reason I can think of is: to trigger some behaviour of the directive that could also be triggered by the user of the application from within the directive.

If so, the obvious and Angulary thing to do would be to broadcast an event on a scope that contains the directive that should react to it. Then the directive would listen to that event and trigger its function by itself.

This has additional benefits:

  • you can send parameters in the event data object if you want to
  • you can trigger a reaction in more than one directive (e.g. if they form a collection)
  • you can communicate with a directive that is deep in the scope hierarchy (e.g. if the directive is inside another directive that is inside other directives, etc.) without passing a function callback through each nested directive
  • it doesn't break the isolation of the directive

Example

Let's try to come up with a very simple example: suppose we have a widget that displays a random inspirational quote downloaded from somewhere. It also has a button to change the quote to a different one.

Here's the directive's template:

<p>{{ quote }}</p>
<button ng-click="refreshQuote()"></button>

And here's the directive's code:

app.directive("randomQuote", function () {
  return {
    restrict: "E",
    scope: {},
    link: function (scope) {
      scope.refreshQuote = function () {
        scope.quote = ... // some complicated code here
      };
      scope.refreshQuote();
    }
  };
});

Note that the directive is entirely self-contained: it has an isolate scope and does the quote-fetching by itself.

Let's suppose we also want to be able to refresh the quote from the controller. This could be as simple as calling this in the controller code:

$scope.$broadcast("refresh-random-quote");

To add the event handler, we must add this code to the link function of the directive:

scope.$on("refresh-random-quote", function () {
  scope.refreshQuote();
});

This way, we've created a one-way communication channel from the controller to the directive that doesn't break the isolation of the directive, and also works if the directive is nested deep in the scope hierarchy of the code that broadcasts the event.

Solution 3

How can I expose a function from directive to the parent controller?
Or: How can I invoke a method on directive from parent controller?

Well, I don't think you should be trying to do this (i.e., coupling controller behavior to a directive), but if you must... here's one way you can do it: pass a controller function to your directive, which the directive can call to notify the controller of the directive function:

<div id="container" my-directive cb="setDirectiveFn(fn)"></div>

directive("myDirective", function() {
    return {
       scope: { cb: '&' },
        controller: function($scope) {
            $scope.myfn = function() {
                console.log("myfn called");
            }
            $scope.cb({fn: $scope.myfn});
        }
    };
});

Fiddle

Solution 4

The release of AngularJS V1.7.1* introduces the new ng-ref directive.

The ng-ref attribute tells AngularJS to publish the controller of a component on the current scope. This is useful for having a component such as an audio player expose its API to sibling components. Its play and stop controls can be easily accessed.

For more information, see

Solution 5

To contribuite, @georgeawg gave me a cool solution using Service to do the job. This way you can handle multiple directives on same page.

<html ng-app="myApp">
<head>
  <script src="https://opensource.keycdn.com/angularjs/1.6.5/angular.min.js"></script>
</head>
<body ng-controller="mainCtrl">
  <h1>^v1.6.0 ($postLink hook required)</h1>
  <my-directive name="sample1" number="number1"></my-directive>
  <my-directive name="sample2" number="number2"></my-directive>
</body>
<script>
  angular.module('myApp', [])
    .controller('mainCtrl', ['$scope', 'myDirectiveFactory', function ($scope, myDirectiveFactory) {
      $scope.number1 = 10
      $scope.number2 = 0
      this.$postLink = function () {
        myDirectiveFactory.get('sample2')
          .increment()
          .increment()
          .increment()
          .increment()
        myDirectiveFactory.get('sample1')
          .increment()
          .increment()
        myDirectiveFactory.get('sample2')
        .decrement()
      }
    }])
    .factory('myDirectiveFactory', function () {
      var instance = {}
      return {
        get: function (name) {
          return instance[name]
        },
        register: function (name, value) {
          return instance[name] = value
        },
        destroy: function (name) {
          delete instance[name]
        }
      }
    })
    .controller('myDirectiveCtrl', ['$scope', 'myDirectiveFactory', function ($scope, myDirectiveFactory) {
      $scope.name = $scope.name || 'myDirective'
      $scope.$on('$destroy', function () {
        myDirectiveFactory.destroy($scope.name)
      })
      var service = {
        increment: function () {
          $scope.number++
          return this
        },
        decrement: function () {
          $scope.number--
          return this
        }
      }
      myDirectiveFactory.register($scope.name, service)
    }])
    .directive('myDirective', [function () {
      return {
        controller: 'myDirectiveCtrl',
        restrict: 'E',
        scope: {
          number: '<',
          name: '@?'
        },
        template: '<p> {{ number }} </p>'
      }
    }])
</script>
</html>
Share:
14,881
Konrad Garus
Author by

Konrad Garus

Quality nut. So disappointed with "good enough" and "I don't care I'm too busy chasing my tail".

Updated on June 05, 2022

Comments

  • Konrad Garus
    Konrad Garus about 2 years

    How can I expose a method from a directive? I know that I should use attributes for data, but I really want to expose behavior, not data. Something that the parent controller can call.

    Let's say my DOM looks like:

    <div ng-app="main">
        <div ng-controller="MyCtrl">
            <button ng-click="call()" >Call</button>
            <div id="container" my-directive> </div>
        </div>
    </div>
    

    JavaScript:

    angular.module("main", []).controller("MyCtrl", function($scope) {
        $scope.call = function() {
            $scope.myfn();
        };
    }).directive("myDirective", function() {
        return {
            // scope: {},
            controller: function($scope) {
                $scope.myfn = function() {
                    console.log("myfn called");
                }
            }
        };
    });
    

    jsFiddle: http://jsfiddle.net/5gDjQ/7/

    If the scope is commented out (i.e. the directive does not have isolated scope), it works just fine. When I press the button, myfn is called and logs to console.

    As soon as I uncomment scope, it doesn't work. myfn is defined on child scope and not easily available to the parent.

    In my case I think that polluting the parent scope is a bad idea and I would really like to avoid it.

    So, how can I expose a function from directive to the parent controller? Or: How can I invoke a method on directive from parent controller?