sfletche sfletche - 5 months ago 15
AngularJS Question

Passing Function Arguments Across Multiple Isolated Scopes with Angular Directives

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
...

Answer

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.

Comments