Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper way to pass functions to directive for execution in link

I know we usually pass functions to directives via an isolated scope:

.directive('myComponent', function () {
    return {
        scope:{
            foo: '&'
        }        
    };
})

And then in the template we can call this function like such:

<button class="btn" ng-click="foo({ myVal: value })">Submit</button>

Where myVal is the name of the parameter that function foo in the parent scope takes.

Now if I intend to use this from the link function instead of template, I will have to call it with: scope.foo()(value), since scope.foo serves as a wrapper of the original function. This seems a bit tedious to me.

If I pass the function to the myComponent directive using =:

.directive('myComponent', function () {
    return {
        scope:{
            foo: '='
        }        
    };
})

Then I will be able to just use scope.foo(value) from my link function. So is this a valid use case to use 2-way binding on functions, or am I doing some sort of hack that I shouldn't be doing?

like image 527
Xavier_Ex Avatar asked Apr 24 '15 21:04

Xavier_Ex


People also ask

What does link function do in a directive?

link function is basically used to manipulate the DOM( Document Object Model ) element using custom directive. link option in custom directive registers DOM listener and also update the DOM.

What is the difference between controller and link in directives?

Answer:The link option is just a shortcut to setting up a post-link function. controller: The directive controller can be passed to another directive linking/compiling phase. It can be injected into other directices as a mean to use in inter-directive communication.

What is a directive function?

Directive Definition. The simplest way to think about a directive is that it is simply a function that we run on a particular DOM element. The directive function can provide extra functionality on the element.


2 Answers

Here is why I downvoted the answer.

First, you should never use '=' to pass function references to directives.

'=' creates two watches and uses them to ensure that both the directive scope and the parent scope references are the same (two-way binding). It is a really bad idea to allow a directive to change the definition of a function in your parent scope, which is what happens when you use this type of binding. Also, watches should be minimized - while it will work, the two extra $watches are unnecessary. So it is not fine - part of the down vote was for suggesting that it was.

Second - the answer misrepresents what '&' does. & is not a "one way binding". It gets that misnomer simply because, unlike '=', it does not create any $watches and changing the value of the property in the directive scope does not propagate to the parent.

According to the docs:

& or &attr - provides a way to execute an expression in the context of the parent scope

When you use & in a directive, it generates a function that returns the value of the expression evaluated against the parent scope. The expression does not have to be a function call. It can be any valid angular expression. In addition, this generated function takes an object argument that can override the value of any local variable found in the expression.

To extend the OP's example, suppose the parent uses this directive in the following way:

<my-component foo="go()">

In the directive (template or link function), if you call

foo({myVal: 42});

What you are doing is evaluating the expression "go()", which happens to call the function "go" on the parent scope, passing no arguments.

Alternatively,

<my-component foo="go(value)">

You are evaluating the expression "go(value)" on the parent scope, which will is basically calling $parent.go($parent.value)"

<my-component foo="go(myVal)">

You are evaluating the expression "go(myVal)", but before the expression is evaluated, myVal will be replaced with 42, so the evaluated expression will be "go(42)".

<my-component foo="myVal + value + go()">

In this case, $scope.foo({myVal: 42}) will return the result of:

42 + $parent.value + $parent.go()

Essentially, this pattern allows the directive to "inject" variables that the consumer of the directive can optionally use in the foo expression.

You could do this:

<my-component foo="go">

and in the directive:

$scope.foo()(42)

$scope.foo() will evaluate the expression "go", which will return a reference to the $parent.go function. It will then call it as $parent.go(42). The downside to this pattern is that you will get an error if the expression does not evaluate to a function.

The final reason for the down vote was the assertion that the ng-event directives use &. This isn't the case. None of the built in directives create isolated scopes with:

scope:{
}

The implementation of '&foo' is (simplified for clarity), boils down to:

$scope.foo = function(locals) {
    return $parse(attr.foo)($scope.$parent, locals);
}

The implementation of ng-click is similar, but (also simplified):

link: function(scope, elem, attr) {
    elem.on('click', function(evt) {
        $parse(attr.ngClick)(scope, {
             $event: evt
        }
    });
}

So the key to remember is that when you use '&', you are not passing a function - you are passing an expression. The directive can get the result of this expression at any time by invoking the generated function.

like image 168
Joe Enzminger Avatar answered Oct 10 '22 22:10

Joe Enzminger


Two-way binding to pass a function is fine as long as your function will always take the same parameters in the same order. But also useless for that purpose.

The one-way binding is more efficient and allows to call a function with any parameter and in any order, and offer more visibility in the HTML. For instance we could not imagine ngClick to be a two-way binding: sometimes you want something like <div ng-click="doStuff(var1)"> up to more complex things such as

<div ng-click="doStuff('hardcoded', var1+4); var2 && doAlso(var2)">

See: you can manipulate the parameters directly from the HTML.

Now I feel like you misunderstood how to use one-way bindings. If you indeed define onFoo: '&' in your directive, then from the link function you should do for instance:

// assume bar is already in your scope
scope.bar = "yo";
// Now you can call foo like this
scope.onFoo( {extra: 42} );

So in your HTML you could use

<div on-foo="doSomething(bar, extra)">

Note that you have access to not only all the properties of the directive isolated scope (bar), but also the extra "locals" added at the moment of the call (extra).

Your notation like scope.foo()(value) looks like a hack to me, that is not the itended way to use one-way bindings.

Note: one-way bindings are typically used with some "event" functions such as when-drop, on-leave, ng-click, when-it-is-loaded, etc.

like image 3
floribon Avatar answered Oct 10 '22 23:10

floribon