13
votes

I have 3 nested, isolate scope, directives (see CodePen here) and I am able to pass a function (that takes an argument) from the outermost directive to the innermost directive (passing the function from outer directive to intermediate directive to inner directive).

What I'm failing to understand is what needs to be done to pass the argument from the inner directive back through intermediate directive back to the outer directive.

Again, see the CodePen example.

Note: Given only 2 isolate scope directives I can get this to work with something similar to the following...

angular.module('myApp', [])
  .directive('myDir1', function() {
    return {
      template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
      controller: function($scope) {
        $scope.addName = function(name) {
          alert(name); // alerts 'Henry'
        };
      }
    }
  })
  .directive('myDir2', function() {
    return {
      scope: {
        addName: '&'
      },
      template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
      controller: function($scope) {
        $scope.testName = 'Henry';
      }
    }
  });

The above code gives me an Alert box with 'Henry' (just like I'd expect).

It's when I add an third, intermediate, isolate scope directive that I run into problems...

angular.module('myApp', [])
  .directive('myDir1', function() {
    return {
      template: '<div><my-dir-2 add-name="addName(name)"></my-dir-2></div>',
      controller: function($scope) {
        $scope.addName = function(name) {
          alert(name); // alerts 'Henry'
        };
      }
    }
  })
  .directive('myDir2', function() {
    return {
      scope: {
        addName: '&'
      },
      template: '<div><my-dir-3 add-name="addName({name: testName})"></my-dir-3></div>',
    }
  })
  .directive('myDir3', function() {
    return {
      scope: {
        addName: '&'
      },
      template: "<span ng-click='addName({name: testName})'>Click to Add {{testName}}!</span>",
      controller: function($scope) {
        $scope.testName = 'Henry';
      }
    }
  });

This code gives me an alert box with undefined...

4

4 Answers

28
votes

A common misconception is that "& is for passing functions". This isn't technically correct.

What & does is create a function on the directive scope that, when called, returns the result of the expression evaluated against the parent scope.

This function takes an object as an argument that will override local variables in the expression with those from the directive scope (the {name: testName}) object in this case.

If you were to look under the hood, the $scope.addName method in myDir2 would look like this (simplified):

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

Your second directive works because the expression it is binding to is

addName(name)

This expression has a local variable name, that is overridden with the value of testName from the directive when executed with

addName({name: testName}) //in the directive. 

Remember - the addName function in myDir2 IS NOT the same as the addName function in myDir1. It is a new function that evaluates the expression

addName(name) 

against the parent scope and returns the result.

When you apply this logic to myDir3, the expression that is evaluated is:

addName({name: testName})

Note that the only local variable in this expression is "testName". So when you call in myDir3 with

addName({name: testName})

there is no local variable name to override, and testName is left undefined.

Phew! No wonder this confuses JUST ABOUT EVERYBODY!

How to fix in your example:

You want the expressions to evaluate to the actual function in myDir1.

angular.module('myApp', [])
  .directive('myDir1', function() {
    return {
      template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
      controller: function($scope) {
        $scope.addName = function(name) {
          alert(name); // alerts 'Henry'
        };
      }
    }
  })
  .directive('myDir2', function() {
    return {
      scope: {
        addName: '&'
      },
      // addName() returns the actual function addName from myDir1
      template: '<div><my-dir-3 add-name="addName()"></my-dir-3></div>',
    }
  })
  .directive('myDir3', function() {
    return {
      scope: {
        addName: '&'
      },
      //calls addName() on myDir2, which returns addName from myDir1, then invokes it passing testName as an argument
      template: "<span ng-click='addName()(testName)'>Click to Add {{testName}}!</span>",
      controller: function($scope) {
        $scope.testName = 'Henry';
      }
    }
  });

Here is the working Pen

Final note - the reason why '&' is more appropriate than '=' here is that '=' is going to actually set up a $watch and two-way bind the variables between the directives. This means that myDir2 could actually change the function appName in myDir1, which is not required and undesirable. It also requires setting up two $watchs, which I try to avoid for performance reasons in Angular.

5
votes

There are two ways to pass a function in an isolated scope. While '&' will make sure that what you are passing is in-fact a function, you can also pass a function as a bound variable with '=', and invoke it only when you need. This method has drawbacks, but it will leave the control of the invocation to the component that is in-charge of that invocation.

Your codepen working

angular.module('myApp', [])
 .directive('myDir1', function() {
    return {
      template: '<div><my-dir-2 add-name="addName"></my-dir-2></div>',
      controller: function($scope) {
        $scope.addName = function(name) {
        alert(name);
      };
    }
  }
})
.directive('myDir2', function() {
  return {
    scope: {
      addName: '='
    },
    template: '<div><my-dir-3 add-name="addName"></my-dir-3></div>'
  }
})
.directive('myDir3', function() {
  return {
    scope: {
      addName: '='
    },
    template: "<span ng-click='addName(testName)'>Click to Add {{testName}}!</span>",      
    controller: function($scope) {
      $scope.testName = "Henry"
    }
  }
});
0
votes

The problem with your code with three nested directives is that testName does not exist in isolated scope of myDir2. You can set that from myDir3, but for that you will have to create an object in scope of myDir2 and set its property to testName and then use that property in myDir2

The working example is here http://codepen.io/anon/pen/Wveqwx

-1
votes

You can access an elements scope using isolateScope()

In the myDir1 directive replace the controller to this instead

  link: function($scope, elem) {
    $scope.addName = function(name) {
      var e = elem.find('my-dir-3');
      var s = e.isolateScope();
      alert(s.testName);
    };

Another way is to potentially use $$nextSibling to get the next scope after current parent (haven't really used this so may want to read up on it)

This is probably not the 'angular way' to do things. I think removing the isolated scope from the child directives and having them reference the parent directive model would be more ideal.