103

I want to create a directive that links to an attribute. The attribute specifies the function that should be called on the scope. But I also want to pass an argument to the function that is determined inside the link function.

<div my-method='theMethodToBeCalled'></div>

In the link function I bind to a jQuery event, which passes an argument I need to pass to the function:

app.directive("myMethod",function($parse) {
  restrict:'A',
  link:function(scope,element,attrs) {
     var expressionHandler = $parse(attrs.myMethod);
     $(element).on('theEvent',function( e, rowid ) {
        id = // some function called to determine id based on rowid
        scope.$apply(function() {expressionHandler(id);});
     }
  }
}

app.controller("myController",function($scope) {
   $scope.theMethodToBeCalled = function(id) { alert(id); };
}

Without passing the id I can get it working, but as soon as I try to pass an argument, the function is not called anymore

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
rekna
  • 5,313
  • 7
  • 45
  • 54
  • [how to access object property via isolate scope without two-way binding?](http://stackoverflow.com/questions/21057320/how-to-access-object-property-via-isolate-scope-without-two-way-binding?answertab=active#tab-top) i think it's helpful. use the isolated scope,and call the parent scope through **$scope.$parent** – JJbang Nov 03 '16 at 12:35

7 Answers7

99

Marko's solution works well.

To contrast with recommended Angular way (as shown by treeface's plunkr) is to use a callback expression which does not require defining the expressionHandler. In marko's example change:

In template

<div my-method="theMethodToBeCalled(myParam)"></div>

In directive link function

$(element).click(function( e, rowid ) {
  scope.method({myParam: id});
});

This does have one disadvantage compared to marko's solution - on first load theMethodToBeCalled function will be invoked with myParam === undefined.

A working exampe can be found at @treeface Plunker

Community
  • 1
  • 1
johans
  • 1,674
  • 12
  • 7
  • Indeed, this pattern of providing the function name with parameter in the custom attribute seems to be the easiest and most robust way for defining "callback functions" in directives... thx !! – rekna Nov 22 '13 at 10:39
  • 3
    It is very confusing to call "setProduct" 2 different things - attribute and scope function, makes it really hard to understand which one is where. – Dmitri Zaitsev Aug 02 '14 at 05:09
  • Whats confusing is why the right answer is using isolate scope but the question does not. Or am i missing something? – j_walker_dev Oct 31 '14 at 10:09
  • From my tests using this code in angular 1.2.28, it works well. My tests do not call theMethodToBeCalled with undefined on first load. Neither does the plunker example. This does not appear to be a problem. In other words, this does appear to be the correct approach when you want to pass parameters in to the directive link (in an isolate scope). I recommend updating the comment regarding the disadvantage and myParam === undefined. – jazeee May 19 '15 at 18:18
  • This "This does have one disadvantage compared to marko's solution - on first load theMethodToBeCalled function will be invoked with myParam === undefined" is not happening in my case.... i guess there is some implicit context here – Victor Dec 05 '16 at 18:52
97

Just to add some info to the other answers - using & is a good way if you need an isolated scope.

The main downside of marko's solution is that it forces you to create an isolated scope on an element, but you can only have one of those on an element (otherwise you'll run into an angular error: Multiple directives [directive1, directive2] asking for isolated scope)

This means you :

  • can't use it on an element hat has an isolated scope itself
  • can't use two directives with this solution on the same element

Since the original question uses a directive with restrict:'A' both situations might arise quite often in bigger applications, and using an isolated scope here is not a good practice and also unnecessary. In fact rekna had a good intuition in this case, and almost had it working, the only thing he was doing wrong was calling the $parsed function wrong (see what it returns here: https://docs.angularjs.org/api/ng/service/$parse ).

TL;DR; Fixed question code

<div my-method='theMethodToBeCalled(id)'></div>

and the code

app.directive("myMethod",function($parse) {
  restrict:'A',
  link:function(scope,element,attrs) {
     // here you can parse any attribute (so this could as well be,
     // myDirectiveCallback or multiple ones if you need them )
     var expressionHandler = $parse(attrs.myMethod);
     $(element).on('theEvent',function( e, rowid ) {
        calculatedId = // some function called to determine id based on rowid

        // HERE: call the parsed function correctly (with scope AND params object)
        expressionHandler(scope, {id:calculatedId});
     }
  }
}

app.controller("myController",function($scope) {
   $scope.theMethodToBeCalled = function(id) { alert(id); };
}
Robert Bak
  • 4,246
  • 19
  • 20
  • 11
    +1 Indeed - no need to isolate the scope - much cleaner! – Dmitri Zaitsev Aug 02 '14 at 05:27
  • what if the attribute contains just the method name, like
    or an inline function like
    – Jonathan. Aug 24 '14 at 17:06
  • 1
    If you are having trouble with any of these approaches, keep in mind that the method being called must be available in the scope you pass to the function returned from `$parse`. – ragamufin Jan 21 '15 at 06:33
  • This answer is exactly what I was looking for +1 – grimmdude May 31 '15 at 21:00
  • what is "theEvent"? How can I execute the function on a child div? – Shlomo Jul 07 '15 at 14:18
  • Something to note, if you are going to use $parse, make sure you are attaching the "theMethodToBeCalled" to the $scope or it will not fire if the method is declared inside a controller of component created with the new .component api in angular. 1.5.x. I ran into this issue. It maybe an issue with a directive that uses controllerAs syntax as well, but I haven't check. – Jose Carrillo Sep 15 '16 at 21:06
88

Not knowing exactly what you want to do... but still here's a possible solution.

Create a scope with a '&'-property in the local scope. It "provides a way to execute an expression in the context of the parent scope" (see the directive documentation for details).

I also noticed that you used a shorthand linking function and shoved in object attributes in there. You can't do that. It is more clear (imho) to just return the directive-definition object. See my code below.

Here's a code sample and a fiddle.

<div ng-app="myApp">
<div ng-controller="myController">
    <div my-method='theMethodToBeCalled'>Click me</div>
</div>
</div>

<script>

   var app = angular.module('myApp',[]);

   app.directive("myMethod",function($parse) {
       var directiveDefinitionObject = {
         restrict: 'A',
         scope: { method:'&myMethod' },
         link: function(scope,element,attrs) {
            var expressionHandler = scope.method();
            var id = "123";

            $(element).click(function( e, rowid ) {
               expressionHandler(id);
            });
         }
       };
       return directiveDefinitionObject;
   });

   app.controller("myController",function($scope) {
      $scope.theMethodToBeCalled = function(id) { 
          alert(id); 
      };
   });

</script>
marko
  • 2,841
  • 31
  • 37
  • The function gets called, when I set $scope.id = id inside theMethodToBeCalled the view is not updated. Probable I need to wrap expressionHandler(id) inside scope.apply. – rekna Jul 10 '13 at 18:06
  • 2
    Awesome this helped me find the solution to my problem. It's worth mentioning that you only need to do this at the top level if you're doing a deeper nest of directives. Consider this: http://plnkr.co/edit/s3y67iGL12F2hDER2RNl?p=preview where I pass the method through two directives. – treeface Oct 29 '13 at 16:45
  • 3
    And here is a second (perhaps more angular) way of doing it: http://plnkr.co/edit/r9CV9Y1RWRuse4RwFPka?p=preview – treeface Oct 29 '13 at 17:01
  • That calls a function on the directive controller, not a different controller. – Adam K Dean Nov 18 '13 at 11:31
  • @treeface right, this is a proper way to pass arguments back to controller – Maxim Shoustin May 06 '14 at 21:18
  • 1
    How can I do it without scope isolation ? – Evan Lévesque Jun 02 '14 at 13:25
  • 3
    +1 because this solution taught me that I need to call scope.method(); first to get the actual reference to the method. – Sal Jun 17 '15 at 11:19
  • @Sal Well, reading description of `&` [binding operator](https://docs.angularjs.org/api/ng/service/$compile#-scope-) it's not clear why it is the case. – Piotr Dobrogost Jan 17 '19 at 10:01
6

You can create a directive that executes a function call with params by using the attrName: "&" to reference the expression in the outer scope.

We want to replace the ng-click directive with ng-click-x:

<button ng-click-x="add(a,b)">Add</button>

If we had this scope:

$scope.a = 2;
$scope.b = 2;

$scope.add = function (a, b) {
  $scope.result = parseFloat(a) + parseFloat(b);
}

We could write our directive like so:

angular.module("ng-click-x", [])

.directive('ngClickX', [function () {

  return {

    scope: {

      // Reference the outer scope
      fn: "&ngClickX",

    },

    restrict: "A",

    link: function(scope, elem) {

      function callFn () {
        scope.$apply(scope.fn());
      }

      elem[0].addEventListener('click', callFn);
    }
  };
}]);

Here is a live demo: http://plnkr.co/edit/4QOGLD?p=info

f1lt3r
  • 2,176
  • 22
  • 26
2

Here's what worked for me.

Html using the directive

 <tr orderitemdirective remove="vm.removeOrderItem(orderItem)" order-item="orderitem"></tr>

Html of the directive: orderitem.directive.html

<md-button type="submit" ng-click="remove({orderItem:orderItem})">
       (...)
</md-button>

Directive's scope:

scope: {
    orderItem: '=',
    remove: "&",
tymtam
  • 31,798
  • 8
  • 86
  • 126
0

My solution:

  1. on polymer raise an event (eg. complete)
  2. define a directive linking the event to control function

Directive

/*global define */
define(['angular', './my-module'], function(angular, directives) {
    'use strict';
    directives.directive('polimerBinding', ['$compile', function($compile) {

            return {
                 restrict: 'A',
                scope: { 
                    method:'&polimerBinding'
                },
                link : function(scope, element, attrs) {
                    var el = element[0];
                    var expressionHandler = scope.method();
                    var siemEvent = attrs['polimerEvent'];
                    if (!siemEvent) {
                        siemEvent = 'complete';
                    }
                    el.addEventListener(siemEvent, function (e, options) {
                        expressionHandler(e.detail);
                    })
                }
            };
        }]);
});

Polymer component

<dom-module id="search">

<template>
<h3>Search</h3>
<div class="input-group">

    <textarea placeholder="search by expression (eg. temperature>100)"
        rows="10" cols="100" value="{{text::input}}"></textarea>
    <p>
        <button id="button" class="btn input-group__addon">Search</button>
    </p>
</div>
</template>

 <script>
  Polymer({
    is: 'search',
            properties: {
      text: {
        type: String,
        notify: true
      },

    },
    regularSearch: function(e) {
      console.log(this.range);
      this.fire('complete', {'text': this.text});
    },
    listeners: {
        'button.click': 'regularSearch',
    }
  });
</script>

</dom-module>

Page

 <search id="search" polimer-binding="searchData"
 siem-event="complete" range="{{range}}"></siem-search>

searchData is the control function

$scope.searchData = function(searchObject) {
                    alert('searchData '+ searchObject.text + ' ' + searchObject.range);

}
venergiac
  • 7,469
  • 2
  • 48
  • 70
-2

This should work.

<div my-method='theMethodToBeCalled'></div>

app.directive("myMethod",function($parse) {
  restrict:'A',
  scope: {theMethodToBeCalled: "="}
  link:function(scope,element,attrs) {
     $(element).on('theEvent',function( e, rowid ) {
        id = // some function called to determine id based on rowid
        scope.theMethodToBeCalled(id);
     }
  }
}

app.controller("myController",function($scope) {
   $scope.theMethodToBeCalled = function(id) { alert(id); };
}
Vladimir Prudnikov
  • 6,974
  • 4
  • 48
  • 57