Can an angular directive pass arguments to functions in expressions specified in the directive's attributes?

138,408

Solution 1

If you declare your callback as mentioned by @lex82 like

callback = "callback(item.id, arg2)"

You can call the callback method in the directive scope with object map and it would do the binding correctly. Like

scope.callback({arg2:"some value"});

without requiring for $parse. See my fiddle(console log) http://jsfiddle.net/k7czc/2/

Update: There is a small example of this in the documentation:

& or &attr - provides a way to execute an expression in the context of the parent scope. If no attr name is specified then the attribute name is assumed to be the same as the local name. Given and widget definition of scope: { localFn:'&myAttr' }, then isolate scope property localFn will point to a function wrapper for the count = count + value expression. Often it's desirable to pass data from the isolated scope via an expression and to the parent scope, this can be done by passing a map of local variable names and values into the expression wrapper fn. For example, if the expression is increment(amount) then we can specify the amount value by calling the localFn as localFn({amount: 22}).

Solution 2

Nothing wrong with the other answers, but I use the following technique when passing functions in a directive attribute.

Leave off the parenthesis when including the directive in your html:

<my-directive callback="someFunction" />

Then "unwrap" the function in your directive's link or controller. here is an example:

app.directive("myDirective", function() {

    return {
        restrict: "E",
        scope: {
            callback: "&"                              
        },
        template: "<div ng-click='callback(data)'></div>", // call function this way...
        link: function(scope, element, attrs) {
            // unwrap the function
            scope.callback = scope.callback(); 

            scope.data = "data from somewhere";

            element.bind("click",function() {
                scope.$apply(function() {
                    callback(data);                        // ...or this way
                });
            });
        }
    }
}]);    

The "unwrapping" step allows the function to be called using a more natural syntax. It also ensures that the directive works properly even when nested within other directives that may pass the function. If you did not do the unwrapping, then if you have a scenario like this:

<outer-directive callback="someFunction" >
    <middle-directive callback="callback" >
        <inner-directive callback="callback" />
    </middle-directive>
</outer-directive>

Then you would end up with something like this in your inner-directive:

callback()()()(data); 

Which would fail in other nesting scenarios.

I adapted this technique from an excellent article by Dan Wahlin at http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters

I added the unwrapping step to make calling the function more natural and to solve for the nesting issue which I had encountered in a project.

Solution 3

In directive (myDirective):

...
directive.scope = {  
    boundFunction: '&',
    model: '=',
};
...
return directive;

In directive template:

<div 
data-ng-repeat="item in model"  
data-ng-click='boundFunction({param: item})'>
{{item.myValue}}
</div>

In source:

<my-directive 
model='myData' 
bound-function='myFunction(param)'>
</my-directive>

...where myFunction is defined in the controller.

Note that param in the directive template binds neatly to param in the source, and is set to item.


To call from within the link property of a directive ("inside" of it), use a very similar approach:

...
directive.link = function(isolatedScope) {
    isolatedScope.boundFunction({param: "foo"});
};
...
return directive;

Solution 4

Yes, there is a better way: You can use the $parse service in your directive to evaluate an expression in the context of the parent scope while binding certain identifiers in the expression to values visible only inside your directive:

$parse(attributes.callback)(scope.$parent, { arg2: yourSecondArgument });

Add this line to the link function of the directive where you can access the directive's attributes.

Your callback attribute may then be set like callback = "callback(item.id, arg2)" because arg2 is bound to yourSecondArgument by the $parse service inside the directive. Directives like ng-click let you access the click event via the $event identifier inside the expression passed to the directive by using exactly this mechanism.

Note that you do not have to make callback a member of your isolated scope with this solution.

Solution 5

For me following worked:

in directive declare it like this:

.directive('myDirective', function() {
    return {
        restrict: 'E',
        replace: true,
        scope: {
            myFunction: '=',
        },
        templateUrl: 'myDirective.html'
    };
})  

In directive template use it in following way:

<select ng-change="myFunction(selectedAmount)">

And then when you use the directive, pass the function like this:

<data-my-directive
    data-my-function="setSelectedAmount">
</data-my-directive>

You pass the function by its declaration and it is called from directive and parameters are populated.

Share:
138,408

Related videos on Youtube

Ed_
Author by

Ed_

Developer &amp; business owner (pixelnebula.com) Always on the lookout for great JavaScript developers - if you're looking for a job in the UK drop an email with your CV to: #SOreadytohelp

Updated on February 02, 2020

Comments

  • Ed_
    Ed_ over 4 years

    I have a form directive that uses a specified callback attribute with an isolate scope:

    scope: { callback: '&' }
    

    It sits inside an ng-repeat so the expression I pass in includes the id of the object as an argument to the callback function:

    <directive ng-repeat = "item in stuff" callback = "callback(item.id)"/>
    

    When I've finished with the directive, it calls $scope.callback() from its controller function. For most cases this is fine, and it's all I want to do, but sometimes I'd like to add another argument from inside the directive itself.

    Is there an angular expression that would allow this: $scope.callback(arg2), resulting in callback being called with arguments = [item.id, arg2]?

    If not, what is the neatest way to do this?

    I've found that this works:

    <directive 
      ng-repeat = "item in stuff" 
      callback = "callback" 
      callback-arg="item.id"/>
    

    With

    scope { callback: '=', callbackArg: '=' }
    

    and the directive calling

    $scope.callback.apply(null, [$scope.callbackArg].concat([arg2, arg3]) );
    

    But I don't think it's particularly neat and it involves puting extra stuff in the isolate scope.

    Is there a better way?

    Plunker playground here (have the console open).

    • Dmitri Zaitsev
      Dmitri Zaitsev over 8 years
      The attribute naming "callback =" misleads. It is really a callback evaluation, not a callback itself.
    • Ed_
      Ed_ over 8 years
      @DmitriZaitsev it's a callback angular expression that will evaluate to a JavaScript function. I think it's fairly obvious that it's not a JavaScript function in itself. It's just preference but I would prefer not to have to suffix all of my attributes with "-expression". This is consistent with the ng API for example ng-click="someFunction()" is an expression that evaluates to executing a function.
    • Dmitri Zaitsev
      Dmitri Zaitsev over 8 years
      I have never seen Angular expression called "callback". It is always a function that you pass to be called, whence the name. You even use a function called "callback" in your example, to make things even more confusing.
    • Ed_
      Ed_ over 8 years
      I'm not sure if you're confused or I am. In my example $scope.callback is set by the callback="someFunction" attribute and the scope: { callback: '=' } property of the directive definition object. $scope.callback is a function to be called at a later date. The actual attribute value is obviously a string - that is always the case with HTML.
    • Dmitri Zaitsev
      Dmitri Zaitsev over 8 years
      You name both attribute and function the same - "callback". That's the recipe for confusion. Easy to avoid really.
  • ach
    ach over 9 years
    Very nice! Is this documented anywhere?
  • Dmitri Zaitsev
    Dmitri Zaitsev about 9 years
    Using scope.$parent makes the directive "leaky" - it "knows" too much of the outside world, which a well-designed encapsulated component shouldn't.
  • ndee
    ndee about 9 years
    A nice approach but I am not able to use the this pointer inside the callback method, because it uses the scope of the directive. I am using Typescript and my callback looks like this: public validateFirstName(firstName: string, fieldName: string): ng.IPromise<boolean> { var deferred = this.mQService.defer<boolean>(); ... .then(() => deferred.resolve(true)) .catch((msg) => { deferred.reject(false); }); return deferred.promise; }
  • OMGPOP
    OMGPOP about 9 years
    I dont think it's a good solution because in directive definition, sometimes you wouldn't know what's the parameter to pass in.
  • lex82
    lex82 almost 9 years
    Well, it knows that it has a parent scope but it does not access a particular field in the scope so I think this is tolerable.
  • Episodex
    Episodex almost 9 years
    Notice: if you have nested directives and want to propagate callback upwards you need to unwrap in each directive, not only the one triggering callback.
  • Wtower
    Wtower over 7 years
    This is a good solution and thank you for that, but I believe the answer needs a bit of tide-up. Who is lex82 and what has he mentioned?
  • trainoasis
    trainoasis over 6 years
    Interesting approach. Although what happens when you want to allow any function with ANY parameter(or multiple) to be passed? You know nothing of the function nor of its parameters and and need to execute it on some event inside directive. How to go about it? For example on a directive you could have onchangefunc='myCtrlFunc(dynamicVariableHere)'
  • Ankit Pandey
    Ankit Pandey over 4 years
    While having In source: bound-function='myFunction(obj1.param, obj2.param)'> then how to proceed?