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

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.

Related

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/

Angular JS custom directive - Attribute binding to string or scope variable

Is it possible when creating a custom directive to have an attribute which can be a string or two way bound to something on the scope?
So, for example if I have this in my directive declaration:
$scope: {
position: '=?'
}
and then alert it in my link function or controller:
$alert($scope.position);
It works if I actually bind it to something on my parent scope, but if I just put a string in I get undefined unless I use single quotes inside the double quotes. E.g.
<my-directive position="'right'"></my-directive>
That way it evaluates the expression as a string, but It seems ugly. I'd rather be able to use position="right" when I want to give the attribute a string, or use position="{{scopeVariable}}" when I want to bind it to two way bind it to something in the parent controller.
Am I wrong to be using "=?" as the isolate scope binding? Is there a better way to do this? Or should I just get used to using single quotes inside double quotes?
= according to Angular documentation represents two-way binding - it means you have to use quotes.
If you are going to pass string your should use text binding:
$scope: {
position: '#'
}
and than you can pass your variable like
<my-directive position="right"></my-directive>
or
<my-directive position="{{right}}"></my-directive>
but remember that it is one way binding.
If your really need 'hybrid' solution which is sometimes a text binding and sometimes two-way binding your could implement your binding manually
{
scope: {}
link: function(scope,element,attr){
attr['position'] // the value of html attribute, use it directly or evaluate in parent scope
scope.$parent.$eval(attr['position']); //evaluate variable 'right' in parent scope
}
}
The difference in # and = syntax may seem confusing. But it is caused by the fact that the latter uses $parse to transform an expression. And the former uses $interpolate, which accepts expression-flavoured string and transforms it using $parse. So attribute with = is always an expression, and Angular doesn't need {{ }} there (it would be position="{{'right'}}" otherwise). And # isn't limited to single expression or static string (I'm not sure that the manual mentioned this clearly).
This behaviour is buried deep inside $compile, and you can't override it in directive, $parse will throw an error because of invalid expression before anything.
To overcome this you need to reimplement = binding in controller or link. All we have to do is to $parse an expression against $scope.$parent, establish proper watcher and remove it on $destroy.

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.

Correct usage of & binding in directives

This has come up for me in answers and generated some discussion and I'd like to figure out if I'm way off base or if there is a "more angular" way to accomplish my goal.
When I write a directive that is going to use an isolated scope, a question that comes up is always =, & or #.
Generally, people always think of & as a way to pass functions to directive. The documentation describes it as:
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}).
I use it for passing functions quite a bit, but I also use it in cases where I want to pass an expression that returns a non-string, and I don't want my directive to be able to modify values in the parent scope. In other words, I use it as:
a way to execute an expression in the context of the parent scope
So if I do not need two-way binding, and the expression is not a string, I will do this:
.directive('displayObject', function() {
scope: {
value: '&=displayObject'
},
template: '<div ng-repeat="(k, v) in value()">{{k}}: {{v}}</div>',
replace: true,
...
});
The directive usage would be:
<div displayObject="someScopePropertyOrExpression"></div>
= isn't ideal here because I do not need two way binding. I don't want my directive modifying the value in the parent scope and I don't want the watch required to maintain it.
# isn't ideal because it interpolates the attribute, so value would always be a string.
::someScopePropertyOrExpression doesn't work because I want the directive template to reflect changes in someScopePropertyOrExpression if it changes.
In each discussion, it's always brought up that
ng-repeat="(key, value) in value()"
sets up a watch - the problem is that = and the template together set up two - one that is wholly unnecessary.
Several times when I've suggested this pattern it's been called a "hack", or a misuse of &, and even "ugly".
I don't think it is any of these, but if it is, then my specific question is what is the alternative?
I've searched quite a bit to prove you wrong in our discussion here, but I think you are right - this is the only way to set up a one-way (from parent to isolate scope) binding to an actual model.
In that aspect "&" is superior to "=", because, as you noted, "=" sets up a watch on the parent's scope whereas "&" does not.
One thing - plunker 1 - (that I finally managed to uncover) that hints on it being a hack is that it does not play nice with Angular's one-time binding foo="::name":
"=" will honor the one-time binding (it sets up a watch on the parent and removes it before the next digest), whereas "&" would not:
<input ng-model="name">
<div two-way="::name">
<div one-way="::name">
The other thing - plunker 2 - is that "&" allows to pass local variables, and they may shadow outer variables:
<input ng-model="name">
<div one-way="name">
But in the directive if you did something like: "{{oneWay({name: 'fooo'})}}", it will cause it to display "fooo", rather than take the value of name from the outer scope.
It's a bit like shooting yourself in the foot, yes, but it does hint that this usage might be slightly outside of its original intent.

Need some examples of binding attributes in custom AngularJS tags

I am attempting to create a custom tag similar to the following:
<mytag type="Big" />
where type is an attribute that gets bound to the component. in such a way that it sets the text in a label, as shown below:
<label>{{type}}</label>
... (other components)...
As the documentation says, I have a controller that sets a default type:
$scope.type = "Small";
so that if I use my tag without the attribute type still gets set.
I am attempting to do binding using a directive:
angular.module('TestPage',[])
.directive('mytag',function() {
return {
restrict: 'E',
templateUrl: 'component.html',
scope: {
type: '='
}
}
});
Note that I do have the appropriate ng-app settings in my component template (ng-app="TestPage").
My problem is that the binding to type does not appear to be actually binding anything.
I have read the documentation about how to bind a variable to components using directive. According to the documentation, you can do such bindings inside a scope. Scopes apparently can contain an "object-hash" (whatever that is!) which creates something called an "isolate scope" (???). Such scopes can represent "local properties" in the following ways:
# or #attr - bind a local scope property to the DOM attribute. The result is always a string
since DOM attributes are strings. If no attr name is specified then the local name and
attribute name are same. Given and widget definition of scope: { localName:'#myAttr' }, then widget scope property localName will reflect the interpolated value of hello {{name}}. As the name attribute changes so will the localName property on the widget scope. The name is read from the parent scope (not component scope).
Huh??? What has all this to do with the proper syntax for binding?
= or =expression - set up bi-directional binding between a local scope property and the parent
scope property. If no attr name is specified then the local name and attribute name are same.
Given and widget definition of scope: { localModel:'=myAttr' }, then widget scope property localName will reflect the value of parentModel on the parent scope. Any changes to parentModel will be reflected in localModel and any changes in localModel will reflect in parentModel.
Excuse me? What is being said here???
& or &attr - provides a way to execute an expression in the context of the parent scope. If no
attr name is specified then the local name and attribute name are same. Given
and widget definition of scope: { localFn:'increment()' },
then isolate scope property localFn will point to a function wrapper for the increment() expression. Often it's desirable to pass data from the isolate 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}).
Now I'm totally confused! You have widget tags and some kind of related function that I have to write iin order to do the bind??? All I want is to bind a value to a label tag!
I have copied the above text from the documentation (http://docs.angularjs.org/guide/directive) to make a point: that this doco reads like the old UNIX documentation: really useful to those who already know the system, but not so helpful to beginners who are trying to develop real expertise. With all the tutorials that show how to do simple tasks in AngularJS (great for toy apps but not so good for the kinds of client- side applications I want to build), why aren't there any for the more advanced stuff???
Okay, time for me to be more constructive.
Can someone please provide some nice, simple examples of how to do the various bindings that this documentation is trying so hard to describe??? Examples that show the proper syntax for these scope statements and descriptions (in plain English) of exactly how they go back to the attribute being added to the custom tag???
Thank you for your patience and thanks in advance for any assistance.
I struggled a bit with this documentation too when first getting into angular, but I will make an attempt try to clarify things for you. First, when using this scope property, it creates an "isolated scope." All this means is that it won't inherit any properties from parent scopes, and so you don't have to worry about any collisions within the scope.
Now, the '#' notation means that the evaluated value in the attribute will automatically get bound into your scope for the directive. So, <my-directive foo="bar" /> would end up with the scope having a property called foo that holds the string "bar". You could also do something like <my-directive foo="{{bar}}" And then the evaluated value of {{bar}} will be bound to the scope. Since attributes are always strings, you will always end up with a string for this property in the scope when using this notation.
The '=' notation basically provides a mechanism for passing an object into your directive. It always pulls this from the parent scope of the directive, so this attribute will never have the {{}}. So, if you have <my-directive foo="bar" /> it will bind whatever is in $scope.bar into your directive in the foo property of your directive's scope. Any change's you make to foo within your scope will be refelected in bar in the parent scope, and vice versa.
I haven't used the '&' notation nearly as much as the other too, so I don't know it as well as those two. From what I understand, it allows you to evaluate expressions from the context of the parent scope. So if you have something like <my-directive foo="doStuff()" />, whenever you call scope.foo() within your directive, it will call the doStuff function in the directive's parent scope. I'm sure there's a lot more you can do with this, but I'm not as familiar with it all. Maybe someone else can explain this one in more detail.
If just the symbol is set in the scope, it will use the same name as the attribute to bind to the directives scope. For example:
scope: {
foo1: '#',
foo2: '=',
foo3: '&'
}
When including the directive, there would need to be the attributes foo1, foo2, and foo3. If you want a property in your scope different than the attribute name, you can specify that after the symbol. So, the example above would be
scope: {
foo1: '#bar1',
foo2: '=bar2',
foo3: '&bar3'
}
When including the directive, there would need to be the attributes bar1, bar2, and bar3, and these would get bound in the scope under properties foo1, foo2, and foo3 respectively.
I hope this helps. Feel free to ask questions with which I can clarify my answer.
Youre pretty close..
app.directive('mytag',function() {
return {
restrict: 'E',
template: '<div>' +
'<input ng-model="controltype"/>' +
'<button ng-click="controlfunc()">Parent Func</button>' +
'<p>{{controlval}}</p>' +
'</div>',
scope: {
/* make typeattribute="whatever" bind two-ways (=)
$scope.whatever from the parent to $scope.controltype
on this directive's scope */
controltype: '=typeattribute',
/* reference a function from the parent through
funcattribute="somefunc()" and stick it our
directive's scope in $scope.controlfunc */
controlfunc: '&funcattribute',
/* pass a string value into the directive */
controlval: '#valattribute'
},
controller: function($scope) {
}
};
});
<div ng-controller="ParentCtrl">
<!-- your directive -->
<mytag typeattribute="parenttype" funcattribute="parentFn()" valattribute="Wee, I'm a value"></mytag>
<!-- write out your scope value -->
{{parenttype}}
</div>
app.controller('ParentCtrl', function($scope){
$scope.parenttype = 'FOO';
$scope.parentFn = function() {
$scope.parenttype += '!!!!';
}
});
The magic is mostly in the scope: declaration in your directive definition. having any scope: {} in there will "isolate" the scope from the parent, meaning it gets it's own scope... without that, it would use the parent's scope. The rest of the magic is in the scope's properties: scope: { 'internalScopeProperty' : '=externalAttributeName' }... where the = represents a two way binding scenario. If you change that = to a # you'll see it just allows you to pass a string as an attribute to the directive. The & is for executing functions from the parent scope's context.
I hope that helps.
EDIT: Here is a working PLNKR

Resources