I recently built a custom directive that needed to call a function in its parent scope and pass some interal variables back as arguments. To be more specific: an editable label – a simple text that turns into an input field when it’s clicked and turns back into plain text, when the focus leaves the input field. On top of that, I needed it to invoke a callback function, if – and only if – the value of the input field has actually been changed.
The main part of the directive was pretty straightforward to build. But implementing the callback to report the change back to the parent scope took me some time. The way it works turned out to be rather unintuitive, but is extremely powerful once you get a hang of it. In this article I’m going to show you how it’s done.
A Simplified Scenario
Let’s say we have function foo
in our $scope
:
$scope.foo = function (newValue, oldValue) {
console.log('Value changed: ' + oldValue + ' to ' + newValue);
}
And a custom directive called bernd
:
angular.module('app', []).directive('bernd', function () {
return {
restrict: 'E',
scope: {
callback: '&'
}
};
});
As you can see, the directive has an isolated scope and provides an attribute callback
which will be bound to the scope as expression thanks to the &
. This is very important, since it basically tells AngularJS to wrap the expression passed via the attribute into a magical function that allows us to do some fancy things. But more about that later.
How to Pass the Function
Now lets see how we can pass foo
to bernd
. The most intuitive way would be to simply pass the function like any other variable:
<bernd callback="foo"/><!-- Be aware: this doesn't work -->
Unfortunately that’s not how it works. Instead we need to pass the function as if we want to invoke it. Remember how I told you about the magical wrapper function AngularJS slaps around the function we pass? At a certain point said wrapper will actually evaluate the given expression. So passing the function like this brings us one step closer:
<bernd callback="foo(newValue, oldValue)"/>
That’s actually all you need to do from a client perspective. But of course our directive has to call the function at some point for this to work. And that’s where it get’s interesting:
var before = 4;
var after = 8;
$scope.callback({
newValue: after,
oldValue: before
});
Instead of passing the arguments to the callback directly, we create a map of argument name to value mappings. Why is that? Remember that we pass foo
as expression in which we actually invoke it. AngularJS simply wraps this expression into a magical little function, which is responsible for providing the environment the expression needs before actually evaluating it. In this case, our expression expects two variables to be present: newValue
and oldValue
. The map we are giving to the wrapper function is simply a map of variable names to values, which we want to make available as local variables.
Some More Details
Internally, AngularJS uses the $parse
service to wrap the given expression into a function that allows it to change the context in which it should be invoked to the parent scope and to pass local variables as map (locals
). The magical wrapper function simply reduces this to passing the locals
. So it’s actually not that magical after all.
This means that the variable names used in the expression are actually a part of your directives public API. They are basically internal variables that you make available to the outside world. You don’t actually pass a function to your directive but simply a piece of code to be executed in the parent scope, which gets access to some predefined local variables. Once you look at it like this, it’s actually a pretty simple concept.
Conclusion
Figuring this all out took me a while, though. Reading the documentation more thoroughly would’ve certainly helped, as the section about the directive definition object clearly states:
[…] 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}).
But somehow I always skipped over this part as I expected to be able to pass and invoke a function reference similar to the two-way binding. I was looking for the wrong thing in all the right places. On the bright side I learend a lot more about the way AngularJS works internally and hopefully created a helpful tutorial by writing this article for those of you, who are trying to figure out the same thing.
Please let me know if this was helpful to you. Either via comment or Twitter @SQiShER.