Angular directive scope evaluation with function binding ('&') - javascript

We are seeing some unexpected behavior in how a function is being bound to a directive scope. Here is a jsbin example.
To summarize - we have a directive that has a scope object as follows:
scope: { fn: '&', val: '#' }
The directive displays the result of fn twice. First we display the result as it is evaluated in the template, then we display the result when evaluated in the link function:
<div><code>fn (&)</code>: {{fn()}}</div>
<div><code>fn result ($scope.result = $scope.fn()) </code>: {{result}}</div>
We then use this scope in another directive:
app.directive('rootDirective', function() {
function link($scope, $elem, $attrs) {
$scope.name = 'directive with scope';
}
return {
scope: 'isolate',
replace: true,
restrict: 'E',
link: link,
template: [
'<div add-scope-directive="">',
' <div ng-repeat="n in [1]">',
' <sub-dir val="{{val}}" fn="fn()" name="{{n}}"></sub-dir>',
' </div>',
' <sub-dir val="{{val}}" fn="fn()" name="{{name}}"></sub-dir>',
'<div>'
].join('\n')
};
});
On the root node of this directive we have another directive add-scope-directive. In this directive we define fn - which returns "add-scope-directive - fn".
We would now expect to see that the result of fn ("add-scope-directive - fn") to be the same throughout the directive. However the result from the link function of the child directive 'sub-dir' when it is not used in a repeater is different - instead it is coming from the function on the MainCtrl.
The question is - are our expectations correct and is this a bug? Or should we be expecting what we see here and if so why?

Not a proper solution, but a workaround could be to put a timeout into the link function of sub-dir, like so:
setTimeout(function() {
$scope.result = $scope.fn();
$scope.$apply();
}, 0);

Related

AngularJs: 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...
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.
There are two ways to pass a function in an isolated scope. While '&' will make sure that what you are passing is in-fact a function, you can also pass a function as a bound variable with '=', and invoke it only when you need. This method has drawbacks, but it will leave the control of the invocation to the component that is in-charge of that invocation.
Your codepen working
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);
};
}
}
})
.directive('myDir2', function() {
return {
scope: {
addName: '='
},
template: '<div><my-dir-3 add-name="addName"></my-dir-3></div>'
}
})
.directive('myDir3', function() {
return {
scope: {
addName: '='
},
template: "<span ng-click='addName(testName)'>Click to Add {{testName}}!</span>",
controller: function($scope) {
$scope.testName = "Henry"
}
}
});
The problem with your code with three nested directives is that testName does not exist in isolated scope of myDir2. You can set that from myDir3, but for that you will have to create an object in scope of myDir2 and set its property to testName and then use that property in myDir2
The working example is here
http://codepen.io/anon/pen/Wveqwx
'myDir2' when you have 3 directives is referencing 'testName', but it doesn't exist in that scope. It was mapped to 'name' in the 'myDir3'. Because of this, changing the template in myDir2 as follows, should fix it:
template: '<div><my-dir-3 add-name="addName({name: name})"></my-dir-3></div>'
You can access an elements scope using isolateScope()
In the myDir1 directive replace the controller to this instead
link: function($scope, elem) {
$scope.addName = function(name) {
var e = elem.find('my-dir-3');
var s = e.isolateScope();
alert(s.testName);
};
Another way is to potentially use $$nextSibling to get the next scope after current parent (haven't really used this so may want to read up on it)
This is probably not the 'angular way' to do things. I think removing the isolated scope from the child directives and having them reference the parent directive model would be more ideal.

AngularJS Directive: Pass dynamically created object as attribute for nested directive

I have a directive that wraps around another directive . The child directive accepts an "options" object as an attribute. I want to create this options object in the parent directive's link function and then set it as an attribute on the child directive in the parent's template, but the options object does not get set if its created dynamically. This works if the options object is set statically in the template itself.
I have a plunker here: http://plnkr.co/edit/gNeKMcneO8RDBmlmpt72?p=preview
Any pointers would be greatly appreciated!!
angular.module('nestedDirectives', [])
.directive('fruitinfo',
[
function() {
return {
restrict: 'A',
scope: {
fruitname: '#?'
},
template: '<br>Fruit Name: {{fruitname}}<br>Fruit Options: {{fruitoptions}}',
link: function(scope, element, attrs) {
scope.fruitoptions = scope.$eval(attrs['fruitinfo']);
}
};
}])
.directive('fruits',
[
function() {
return {
restrict: 'E',
scope: {
selectedFruits: '=?',
btnSizeClass: "#?"
},
template: 'btnSizeClass: {{btnSizeClass}}<br>Fruits: {{fruits}}<br><div ' +
' fruitinfo="fruitOptions" ' +
' fruitname="{{f}}"' +
' ng-repeat="f in fruits">' +
'</div><br><br>' +
'<div fruitname="With static fruitOptions: {{f}}" fruitinfo="{test: \'testOption\', btnSizeClass: \'btn-xs\'}" ng-repeat="f in fruits"></div>',
link: function(scope, element, attrs) {
scope.fruitOptions = {test: 'testOption', btnSizeClass: scope.btnSizeClass};
scope.fruits = ['Apple', 'Banana', 'Watermelon', 'Strawberry'];
}
};
}]
)
;
any particular reason why you are using $eval instead of using "&" in your scope definition like this
http://plnkr.co/edit/W47LZsQ3i4zS8Feu7sDl?p=preview
if you use
fruitoptions:'&fruitinfo'
and then you do
scope.fruitoptions=$scope.fruitoptions()
in your link function you'll get the evaluated expression in its original scope, also consider doing this on the controller function which is invoked prior the link cycle
I figured it out. The "fruitOptions" value must be serialized so that the template can compile it as an attribute, which can then be converted back to an object using "eval" in the nested directive. Plunker updated.

Call function on directive parent scope with directive scope argument

I am developing a directive which shows and hides it's contents based on a click event (ng-click) defined in it's template. On some views where the directive is used I'd like to be able to know if the directive is currently showing or hiding it's contents so I can respond to the DOM changes. The directive has isolated scope and I am trying to notify the parent scope when the directive has been "toggled". I'm attempting to accomplish this by passing a callback function to the directive where it is used that can be called when the directive's state changes i.e hides or shows
I'm not sure how to correctly implement this being that the state of the directive (hidden or shown) is stored in the directive's isolated scope and is determined after the ng-click. Therefore I need to call the parent scope's function from within the directive and not from withing the view.
This will make WAAY more sense with an example. Here is a plunked demonstrating what I'd like to do:
http://plnkr.co/edit/hHwwxjssOKiphTSO1VIS?p=info
var app = angular.module('main-module',[])
app.controller('MainController', function($scope){
$scope.myValue = 'test value';
$scope.parentToggle = function(value){
$scope.myValue = value;
};
});
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="toggle();">Click Me</a>',
replace: true,
scope: {
OnToggle: '&'
},
link: function($scope, elem, attrs, controller) {
$scope.toggleValue = false;
$scope.toggle = function () {
$scope.toggleValue = !$scope.toggleValue;
$scope.OnToggle($scope.toggleValue)
};
}
};
});
I'm relatively new to Angular. Is this a bad idea to begin with? Should I be using a service or something rather than passing around function refs?
Thanks!
Update
You can also use & to bind the function of the root scope (that is actually the purpose of &).
To do so the directive needs to be slightly changed:
app.directive('toggle', function(){
return {
restrict: 'A',
template: '<a ng-click="f()">Click Me</a>',
replace: true,
scope: {
toggle: '&'
},
controller: function($scope) {
$scope.toggleValue = false;
$scope.f = function() {
$scope.toggleValue = !$scope.toggleValue;
$scope.toggle({message: $scope.toggleValue});
};
}
};
});
You can use like this:
<div toggle="parentToggle(message)"></div>
Plunk
You could bind the function using =. In addition ensure the property name in your scope and tag are matching (AngularJS translates CamelCase to dash notation).
Before:
scope: {
OnToggle: '&'
}
After:
scope: {
onToggle: '='
}
Furthermore don't use on-toggle="parentToggle({value: toggleValue})" in your main template. You do not want to call the function but just passing a pointer of the function to the directive.
Plunk

AngularJs: Calling contoller method from directive inside another directive

I have a directive inside another directive. Outer directive shares its scope with controller, while inner one has its own. I'd like to pass a reference to controller's function to inner directive so it can be called from there. But I cannot figure out how to pass the function and its parameters to inner directive so it can properly call the controller's function.
Here is planker to illustrate my problem.
If you click on "Dir 2 Click me" the alert says the parameters have came undefined.
You can pass in the outer controller method using '=' and adjust the code accordingly...
angular.module('app', [])
.controller('ctrl', function($scope){
$scope.myCtrlMethod = function(msg, b) {
alert(msg + ' and b='+b);
};
})
.directive('dir1', [function(){
return {
restrict: 'E',
replace: true,
template: '<div><p ng-click="myDir1Method(\'my dir1 method\',\'b\')">Dir 1 Click me</p><dir2 my-ctrl-method="myCtrlMethod"></dir2></div>',
link: function(scope, elem, attrs){
scope.myDir1Method = function(msg,b){
scope.myCtrlMethod(msg, b);
};
}
};
}])
.directive('dir2', [function(){
return {
restrict: 'E',
scope: {
myCtrlMethod: '='
},
replace: true,
template: '<p ng-click="myDir2Method(\'my dir2 method\',\'b\')">Dir 2 Click me</p>',
link: function(scope, elem, attrs){
scope.myDir2Method = function(msg,b){
scope.myCtrlMethod(msg, b);
};
}
};
}]);
Plunker: http://plnkr.co/edit/xbSNXaSmzWa3G1GSH6Af?p=preview
Edit: '=' evaluates the expression in the context of the parent scope and its result is bound to the property on the inner scope. In this example, 'myCtrlMethod' is evaluated against the parent scope, which returns myCtrlMethod from the parent scope (a function). This function is bound to myCtrlMethod on the inner scope, and can be invoked with scope.myCtrlMethod(msg, b).
you can use controller as a reference to your directive
See: http://jsbin.com/vayij/1/edit
directive('sonDirective', function(){
return {
restrict: 'E'
scope: {},
replace: true,
template: '<div....'
controller: 'MainController' //controller as a reference
}
})
Just put the controller on the scope : $scope.$b=this;
See : http://plnkr.co/edit/skDF8D1scFJYrTUmcXIL?p=preview

Modify template in directive (dynamically adding another directive)

Problem
Dynamically add the ng-bind attribute through a custom directive to be able to use ng-bind, ng-bind-html or ng-bind-html-unsafe in a custom directive with out manually adding to the template definition everywhere.
Example
http://jsfiddle.net/nstuart/hUxp7/2/
Broken Directive
angular.module('app').directive('bindTest', [
'$compile',
function ($compile) {
return {
restrict: 'A',
scope: true,
compile: function (tElem, tAttrs) {
if (!tElem.attr('ng-bind')) {
tElem.attr('ng-bind', 'content');
$compile(tElem)
}
return function (scope, elem, attrs) {
console.log('Linking...');
scope.content = "Content!";
};
}
};
}]);
Solution
No idea. Really I can not figure out why something like the above fiddle doesn't work. Tried it with and with out the extra $compile in there.
Workaround
I can work around it might adding a template value in the directive, but that wraps the content in an extra div, and I would like to be able to that if possible. (See fiddle)
Second Workaround
See the fiddle here: http://jsfiddle.net/nstuart/hUxp7/4/ (as suggested by Dr. Ikarus below). I'm considering this a workaround for right now, because it still feels like you should be able to modify the template before you get to the linking function and the changes should be found/applied.
You could do the compiling part inside the linking function, like this:
angular.module('app').directive('bindTest', ['$compile', function ($compile) {
return {
restrict: 'A',
scope: true,
link: {
post: function(scope, element, attrs){
if (!element.attr('ng-bind')) {
element.attr('ng-bind', 'content');
var compiledElement = $compile(element)(scope);
}
console.log('Linking...');
scope.content = "Content!";
}
}
};
}]);
Let me know how well this worked for you http://jsfiddle.net/bPCFj/
This way seems more elegant (no dependency with $compile) and appropriate to your case :
angular.module('app').directive('myCustomDirective', function () {
return {
restrict: 'A',
scope: {},
template: function(tElem, tAttrs) {
return tAttrs['ng-bind'];
},
link: function (scope, elem) {
scope.content = "Happy!";
}
};
});
jsFiddle : http://jsfiddle.net/hUxp7/8/
From Angular directive documentation :
You can specify template as a string representing the template or as a function which takes two arguments tElement and tAttrs (described in the compile function api below) and returns a string value representing the template.
The source code tells all! Check out the compileNodes() function and its use of collectDirectives().
First, collectDirectives finds all the directives on a single node. After we've collected all the directives on that node, then the directives are applied to the node.
So when your compile function on the bindTest directive executes, the running $compile() is past the point of collecting the directives to compile.
The extra call to $compile in your bindTest directive won't work because you are not linking the directive to the $scope. You don't have access to the $scope in the compile function, but you can use the same strategy in a link function where you do have access to the $scope
You guys were so close.
function MyDirective($compile) {
function compileMyDirective(tElement) {
tElement.attr('ng-bind', 'someScopeProp');
return postLinkMyDirective;
}
function postLinkMyDirective(iScope, iElement, iAttrs) {
if (!('ngBind' in iAttrs)) {
// Before $compile is run below, `ng-bind` is just a DOM attribute
// and thus is not in iAttrs yet.
$compile(iElement)(iScope);
}
}
var defObj = {
compile: compileMyDirective,
scope: {
someScopeProp: '=myDirective'
}
};
return defObj;
}
The result will be:
<ANY my-directive="'hello'" ng-bind="someScopeProp">hello</ANY>

Categories