35

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?

Dzinx
  • 55,586
  • 10
  • 60
  • 78
Konrad Garus
  • 53,145
  • 43
  • 157
  • 230
  • You could simply `$scope.$parent.myfn = function() { ... }`, buut you got some better answers down there. – Langdon Apr 10 '13 at 20:16

5 Answers5

27

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");
            }
        }
    };
});
Roy Truelove
  • 22,016
  • 18
  • 111
  • 153
  • 7
    Is this the best Angular has to offer? I mean, if there is more than one directive between the top controller, and the bottom directive, then one has to pass the function explicitly through all of the middle directives. And exposing function (that needs to access top level $scope data) on top controller seems to be a very basic idea as an Angular newbie. – egaga Mar 11 '14 at 18:42
  • Actually when you do it explicitly you can understand what is going on and which variables from the controllers scope you use. Anotherway you may face with problem when you have some "magic" side effects happening in your directive. So I always try to use isolate scope in my project - it will point me in future what I exactly need to properly use this directive. – Artemis Sep 14 '14 at 11:24
  • Got this to work for a directive of type Attribute but not of type Element. I posted that as a new question at http://stackoverflow.com/questions/29638426/how-to-expose-behavior-from-element-directive. – Michael Osofsky Apr 14 '15 at 22:24
  • 1
    If you do not define `my-fn` attribute on the directive, you will get an error stating that 'undefined' (`my-fn`) is non-assignable. – alans May 11 '15 at 18:36
  • One problem I have with this is the implicit `fnInCtrl`. If I were to see that in the controller, my first thought would be "where the heck is this defined?", meaning it should be commented, meaning there ought to be a better design -.- – aaaaaa Aug 20 '15 at 21:46
  • Which version of angular is that? Doesn't seem to work in 1.4.3 – Valery Gavrilov Sep 15 '19 at 01:25
17

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.

Dzinx
  • 55,586
  • 10
  • 60
  • 78
  • This solution has two serious limitations: 1) An event is broadcasted, so in case there are multiple directives all will be notified. Is there an elegant way to notify specific directive? 2) There is no way for directive to return a value, since it's an event and not a function call. – Maxim Kulikov Sep 29 '16 at 16:19
  • @MaximKulikov you're right on both points. These are the disadvantages of this solution. Point 1 could be used as an advantage (if you want all directives of the same type to react to one event). Otherwise, you could indicate in event data (2nd argument to `$broadcast`), which directive should react to it. I don't believe there is a good workaround for point 2. The directive could `$emit` the answer, but I wouldn't recommend that. – Dzinx Oct 03 '16 at 11:37
7

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

Mark Rajcok
  • 362,217
  • 114
  • 495
  • 492
  • 1
    I notice both your and Roy's answer created a separate attribute. Is that any better than just saying `my-directive="fn"`? – Langdon Apr 10 '13 at 20:11
  • 1
    @Langdon, I tend to like using additional attributes to pass things to my directives, since the names of those attributes can help someone reading the HTML understand better what is being passed. Obviously you need to use attributes if you need to pass more than one thing to the directive. I think it is more of a style thing if you only have one thing to pass. – Mark Rajcok Apr 10 '13 at 20:14
  • re: "I don't think you should be trying to do this (i.e., coupling controller behavior to a directive)" isn't essentially as passing a callback to a 3rd party library? Your callback code has to be specific to what that 3rd party needs – Roy Truelove Apr 10 '13 at 20:24
  • 1
    @Roy, I see see your point, but I feel the directive case is somewhat different. Normally we try to wrap 3rd-party components in a directive. Any callback function is ideally part of the directive, so the coupling is limited to the wrapper (directive). In Konrad's case, I think it would be better to put the ng-click into the directive's template and let it handle the click behavior. – Mark Rajcok Apr 10 '13 at 20:34
  • For some reason I've yet to comprehend, Roy's solution didn't work for me so I resorted to this and it works. Thanks! :) – Bernhard Hofmann Jun 17 '15 at 09:48
2

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

georgeawg
  • 48,608
  • 13
  • 72
  • 95
1

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>
saulsluz
  • 94
  • 10