Making isolated scope one-way data binding for expressions optional - angularjs

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

Related

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

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

Trying to figure out how a directive works, to design a test for it

I'm using Jasmine+Karma and need to find a way to test an angular directive used to alert the user if passwords don't match - it seems to accomplish this with a directive the renders true or false, and there is with ngShow on the HTML that displays when this, along with a couple other properties, are true.
Here's the directive. I'm having a little difficulty understanding how it works.
app.directive('passwordMatch', [function () {
return {
restrict: 'A',
scope:true,
require: 'ngModel',
link: function (scope, elem, attrs, control) {
var checker = function () {
var e1 = scope.$eval(attrs.ngModel);
var e2 = scope.$eval(attrs.passwordMatch);
if(e2!=null)
return e1 == e2;
};
scope.$watch(checker, function (n) {
control.$setValidity("passwordNoMatch", n);
});
}
};
}]);
<small class="errorMessage" data-ng-show="signupForm.password2.$dirty && signupForm.password2.$error.passwordNoMatch && !signupForm.password2.$error.required"> Password do not match.</small>
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM?
How does it do that, then when the purpose is to detect if passwords do not match - if they don't match, then e1 === e2 is false, and this value is passed into $scope.watch(checker, function(n)...? If that was how it worked, then wouldn't it set the of value passwordNoMatch to false, which would make ng-show hidden?
Or is that not how it works, it works another way?
And, before that, what's going on with the link: function part?
Where is the scope coming from (it just says scope:true in the directive?)
And the elem? And the attr (the attributes from html elements?)?
Is angular just looking through a list of each and every one of them, the elements and attributes, and the scope? Is there a passwordMatch property already in there somehow?
What is $eval doing?
You have a lot of questions here and spending some time with the Angular docs would help you answer a lot of them. I'll put links to the relevant docs so you can get a fuller explanation.
So as far as I'm able to tell, what's happening is scope.$watch is watching the checker function for a change, which then gets put into the listeners argument and updates the property on the DOM? How does it do that?
I think you are almost there. When you watch a function it gets called on every digest cycle and if the return value of the function has changed then it calls the function you passed as the second argument i.e.
function(n){
control.$setValidity("passwordNoMatch", n);
}
To understand this function you need to understand what the require option does. You have set require to 'ngModel' and basically what this means is that the control variable passed in as the 4th argument to your link function is a reference to NgModelController, which provides an API for the ngModel directive.
The $setValidity("passwordNoMatch", n) bit sets the value of the property 'passwordNoMatch' on the $error object to the value of n. So, n here is the return value of the function you are watching and the $error object is a property of the FormController that is available on all angular forms which have the name attribute defined in the HTML form tag. So, basically this function is what sets the value of the signupForm.password2.$error.passwordNoMatch that you see in your <small> tag.
Where is the scope coming from (it just says scope:true in the directive?)
The scope in link function is (from the Angular docs)..
The scope to be used by the directive for registering watches.
The scope: true bit is what tells Angular whether to create a new scope for the directive or to create an 'isolate scope' which does not 'prototypically inherit from the parent scope'. I would recommend that you spend some quality time reading about the directive definition object if you really want to grok directives.
What is $eval doing?
The first argument passed to scope.$eval is executed as an Angular Expression and the result is returned. So in your code I suspect they will both return strings from your password fields which you then check to see if they match.
Hope that helps.

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.

Why can't we use interpolation expression with "ng-model" but can use with "ng-src"?

I am trying to use the interpolation expression with the ng-model directive but it doesn't work. On the contrary, when I use interpolation with ng-src, it works perfectly fine. What is this difference due to?
It all depends upon how the directive has been setup.
Some directives like ng-model, ng-show and ng-click do not use interpolation symbols whereas directives ng-src take interpolation.
Interpolation are supported on directives that work only with strings. If we look at ng-src implementation you will find
attr.$observe(normalized, function(value) {
if (!value)
return;
attr.$set(attrName, value);
if (msie) element.prop(attrName, attr[attrName]);
});
attr.$observe watches for change in attribute not model. Model changes cause attribute changes (due to interpolation), hence causing the trigger to fire.
For all other directives like ng-model, the attribute value is an expression which is evaluated in current scope and is not limited to string value.
If you are developing your own directives, isolated scope properties = and # help you achieve something similar.
It's to do with when the code in the directive looks at the attribute: before or after interpolation.
I suspect ngModel uses the attribute, and by that I mean passing it to $parse or $eval, as it is before interpolation.
However, the directive ngSrc uses the attribute, and by that I mean setting the src attribute or not to the attribute value, after interpolation.
The reason as to why ngModel uses the value before interpolation, but ngSrc uses it after, I suspect is to do with how they're coded up. Consider the directive
<my-directive my-attr="{{ 'name' }}"></my-directive>
coded up as:
app.directive('myDirective', function() {
return {
restrict: 'E',
link: function($scope, $element, $attrs) {
console.log('Link:', $attrs.myAttr); // Outputs "name"
},
controller: function($scope, $element, $attrs) {
console.log('Controller:', $attrs.myAttr); // Outputs "{{ 'name' }}"
}
};
});
The un-interpolated value is seen in the controller, but the post-interpolated value is seen in the linking function. So the order of the (relevant) events is
controller,
then interpolation,
then linking function.
You can see this example in a Plunker. I suspect ngModel uses the value from the controller, and therefore sees the pre-interpolated value, but ngSrc uses the linking function, and so sees the post-interpolated value.
One reason as to why ngModel uses the controller rather than the linking function, I suspect is so that it can expose the controller to other directives, which can use it via their require option.
A slightly confounding complication is that ngSrc doesn't expect the attribute to be a valid Angular expression. Rather than passing it through $parse or $eval, it just uses the post-interpolated value of the attribute directly. (i.e. it doesn't need the URL to be wrapped in quotes).
If you wanted to, you could write a directive that combines interpolation with Angular expressions. From the linking function you can pass the attribute to $parse or $eval. If the post-interpolated value is a valid Angular expression, this would work. You could do the same thing from the controller, but you would have to pass the value through $interpolate first.
As you have discovered, ngModel does't support this. However, I don't see why you couldn't write some basic version of ngModel that supports both, and so could be used as
my-model="myScope{{ 'VariableName' }}"
that accesses the scope variable myScopeVariableName.
To bring it back tot the question again, why ngModel itself doesn't support this, I suspect is because use cases are limited (the OP didn't mention one in the question), and so would add complexity to the directive without good reason.
It has to do with one way vs two way data binding provided by directives isolated scope. Check out "Isolated Scope Explained" section in the follwoing post:
http://www.undefinednull.com/2014/02/11/mastering-the-scope-of-a-directive-in-angularjs/

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