Angular directive does not evaluate simple expression when using the & operator - angularjs

I have a directive with an isolate scope configured like so:
scope: {
title: '#',
icon: '#',
onSelect: '&'
},
When I provide a simple expression like:
<my-component on-select="alert('test')"></my-component>
The expression is not evaluated (does not fire).
However, the following does work:
<my-component on-select="vm.someValue = true"></my-component>
And if I move my simple alert call into a function in my controller and pass that, it also works:
<my-component on-select="vm.sendAlert()"></my-component>
The behaviour seems a bit inconsistent. Can someone explain the requirements of expression bindings?

As mentioned by JB Nizet, expressions inside of an angular binding are evaluated on scope.
Angular expressions do not have access to global variables like window, document or location. This restriction is intentional. It prevents accidental access to the global state – a common source of subtle bugs.
Therefore angular only allows you to use simple operators and functions on scope inside of bindings.
https://docs.angularjs.org/guide/expression

If you need to pass a function into a directive as a parameter such that the directive can call it, this is the best way I've found to get it to work.
$scope.myfunction = function(msg) { alert(msg); }
<my-component on-select="myfunction"/>
Directive:
scope : { onSelect: '=' } // Notice using '='
scope.doCall = function() { onSelect("some message"); } // Call the passed in function

Related

What are $locals in AngularJS expressions?

AngularJS Developer Guide on Expressions mentions something named $locals:
It is possible to access the context object using the identifier this and the locals object using the identifier $locals.
I don't understand what "locals object" is and I can't find any more info on $locals in docs. What is it's purpose? How do you operate on it?
The relevant commit where to find more information about it is this one, which also links to the issue asking to introduce $locals.
In short, when passing a parameter to a directive using '&', in order for the directive to be able to execute some code when needed (for example when you use ng-click="doSomething()"), the directive can pass information to the caller using local values.
For example you can use ng-click="doSomething($event)", where $event is not an attribute of the scope, but a value passed by the ng-click directive.
Instead of accessing each "local" value passed by the directive individually, you can have access to all of them at once using $locals.
More information on how to pass local values from the directive is available in the documentation on directives:
& 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 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})
(emphasis mine)
In the above axample, the entire object {amount: 22} passed by the directive is available using $locals, and you could thus use increment($locals.amount)
See this test describing functionality that came about from Issue 13247.
Also consider this example of a directive with a callback:
// JS
angular.module('app', [])
.controller('AppCtrl', function($scope) {
$scope.log = function(locals) {
console.log(locals);
};
})
.component('fooBar', {
template: "",
bindings: { cb: '&'},
controller: function() {
this.a = 1;
this.b = 2;
this.c = 3;
this.cb(this);
}
});
// HTML
<body ng-app="app" ng-controller="AppCtrl">
<foo-bar cb="log($locals)">foobar</foo-bar>
</body>

AngularJS Programmatic binding

I have a directive that accepts a variable passed to it...
scope: {
myindex: '='
},
I then typically it use it like this, and all is good...
<my-dir myindex="index"></my-dir>
However if I were to generate my directive programmatically like this...
var content = $compile(template)(scope);
element.append(content);
How can I set the myindex variable?
Expressions are evaluated on the scope. $index in the view actually means scope.$index.
I don't really understand what you mean by "generating a directive programmatically", but if the template that you compile uses a variable $index, and you want this variable to have the value 42, then what you need is
scope.$index = 42;

AngularJS - alternative use for isolated scope expressions ("&")

I know "&" binding used to pass the method (expressions) to directives isolate scope, so the directive will able to execute it when needed.
Many times I need to "pass" the same expression from my main controller, more than one level deep, to nested directive (2-3 levels). This why on my own, I don't like to use "&" for that purpose. For me, sending "callbacks" using "=" bindings works much better. But this is not a question.
The question is:
What for, I can use "&" in addition to passing functions?
Can I have something like this: my-directive-click="clickCount +=1"?
& is more about allowing you to deal with expressions, and more importantly (in my eyes) allows you to place parameters into the calling parent's scope.
For instance if you have:
scope: {
something: '&'
}
and then in that directives template you could do:
<select ng-model="selection" ng-change="something({$item: selection})" ...>
The caller/user of this directive than can ACCESS $item in the expression passed to something, i.e. $item is placed on its scope.
e.g.
<my-dir something="myOwnVar = $item + 1"></my-dir>
here is a plunker with an example of this, including chaining (multiple nested calls of a & expression):
http://plnkr.co/edit/j4FCBIx0FVz4OT0w50bU?p=preview
In reality, the & is meant as one-way-data-binding.
So = is two-way-data-binding which means changes done on the directive will persist to the original object.
# is just a string.
And & is special. The thing is that it creates a getter for your value, in the case of an invoked function, the getter actually calls the function. I tend to do this on the DDO:
.directive('myDirective', function() {
return {
restrict: 'E',
scope: {
getParameters: '&?params'
}
So in this way, the value bound to the scope is getParameters (so it's clear is a getter) but on the directive element you will only refer it as params:
<my-directive params="ctrl.params">
Your question is vague though and even though you may be able to do what you were asking, I think it would be best to do that inside the directive rather than the way you proposed.
I hope this helped.

Making isolated scope one-way data binding for expressions optional

In my directive I created an isolated scope binding an expression to an attribute:
scope: {
foo: '#'
}
And in the directive if I try to console log scope.foo, I will get the following function:
function (locals) {
return parentGet(scope, locals);
}
And then executing scope.foo() will invoke the expression in the parent's scope. The problem is, the expression I am passing in is an optional callback, but I have no safe way of telling if the attribute is defined from the parent. I will get the function no matter what expression the attribute holds, even if it wasn't specified.
As a result, testing for the existence of scope.foo obviously doesn't help, and I can't test for scope.foo() because that will evaluate the expression if there is one. Is there a good way to make the expression binding optional? I wish we have something similar to '=?' when binding expressions, but there doesn't seem to be a '&?'.
This is where $parse comes in handy. It delegates the tasks of determining which scope and if it's an expression or a constant. Combining this with a quick check to see if the attribute is even there you could do something like so:
.directive('myDirective', function($parse){
return {
link: function($scope, elem, attrs){
if (attrs.foo){
var fooGetter = $parse(attrs.foo)
var callback = fooGetter($parse)
if (callback) callback.call()
}
}
}
})
Here are the docs on that - $parse docs
But my beef with this is that if you are doing any kind of controller-as implementation combined with a callback, you ofttimes get javascript scope issues when using this . I'd much prefer using the optional scope callback which is exactly why this exists, for calling methods on another scope:
scope: { foo: '&?'}
Not sure what version of Angular you're using but that is an option as I too had sought a similar solution - Optional two-way binding on isolate scope for Angular Directive

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

Resources