AngularJS isolate scope: & vs = - angularjs

I'm trying to learn AngularJS, but there's one thing I can't wrap my head around.
What is the benefit of using "&" over "="? All the tutorials, demonstrations and documentation I've found tell me that & evaluates the expression in the parent scope. However, when I first tried doing callbacks to my controller from my directive, I just used a = binding to a function on the parent scope, and it worked fine.
For example, with a function on the controller's scope foo that takes a parameter bar, I could do a directive declaration like
scope: { callback: '=' },
template: '<div ng-click="callback(value)"></div>'
and include the directive like
<my-directive callback="foo"></my-directive>
To achieve the same with &, it seems like I have to do
scope: { callback: '&' },
template: '<div ng-click="callback({bar:value})"></div>'
and
<my-directive callback="foo(bar)"></my-directive>
From this, I don't really see the advantage. Have I misunderstood &?
EDIT: I suppose a valid addition to my question is: Is it a bad idea to bind to parent scope functions using = instead of &?

The difference it seems is that with the & binding, the user of the directive fixes what function is called on a parent scope and which parameters are used, while an = binding simply means passing a reference function reference which the directive can call with whatever arguments.
= isn't meant for that though, it exists mainly for synchronizing properties between nested scopes, while & is meant give the directive a way to interact with the 'outside' world without having knowledge about the outside world.
example of both
<div ng-app="app" ng-controller="ParentCtrl as parentCtrl">
<bind-dir func-is="parentCtrl.func" func-and="parentCtrl.func(arg)"></bind-dir>
</div>
var app = angular.module('app', []);
app.controller('ParentCtrl', function () {
this.func = function (arg) {
alert(arg);
}
})
app.directive('bindDir', function () {
return {
scope: {
funcIs: '=',
funcAnd: '&'
},
template: '<button ng-click="funcIs(\'=\')">=</button><button ng-click="funcAnd({arg:\'&\'})">&</button>'
}
});

Official docs.
# or #attr - bind a local scope property to the value of DOM attribute. The result is always a string since DOM attributes are strings.
= or =attr - set up bi-directional binding between a local scope property and the parent scope property of name defined via the value of the attr attribute.
& or &attr - provides a way to execute an expression in the context of the parent scope.
Further reading: Understanding-Scopes.

'=' gives two way data binding. This means that if you alter the value of the expression within the directive it also alters in the controller. This is called polluting the controller.
'&' is nicer and more modular. You can pass in the function and it will be executed in the scope of the controller but you are unable to change the function in the controller scope.
Please see http://jsfiddle.net/b6ww0rx8/ and it will be a bit clearer.
<div ng-app="myApp">
<div ng-controller="MyController">
<div my-directive
callback1="aFunction"
callback2="anotherFunction()">
</div>
<button ng-click="test()">click me</button>
</div>
</div>
angular.module('myApp', [])
.controller('MyController', function($scope) {
$scope.aFunction = function() {
console.log('abc');
};
$scope.anotherFunction = function() {
console.log('def');
};
$scope.test = function () {
console.log($scope.aFunction);
console.log($scope.anotherFunction);
};
console.log($scope.aFunction);
console.log($scope.anotherFunction);
})
.directive('myDirective', function(){
return {
scope: {
callback1: '=',
callback2: '&'
},
link: function (scope, element, attrs) {
scope.callback1 = 123;
scope.callback1 = 456;
}
}
});

Is the capability to execute an expression against the parent's context a benefit for you? The first example below executes myLocalModel as a function, unlike the '=', you get the result already.
template: "{{ myLocalModel() }}"
begin added update 01
example, you might have 2 attributes with expression, and you only want to execute either one depending on a condition. that could save execution time. is it beneficial for you?
end update 01
the '&'
, Execute expression against a parent's context.
http://ngtutorial.com/learn/directive.html#/exec-expr
angular.module("myApp", []).directive("myCustom", function(){
return {
restrict: 'EA',
scope: {
myLocalModel: '&theElementsAttrName',
},
// note that myLocalModel is a function
template: "{{ myLocalModel() }}"
};
});
........
<body ng-app="myApp">
<div ng-init="ParentModel='the parents value';
ParentNum1=100;
ParentNum2=200"></div>
<div ng-controller="CreateChildScopeController">
my-custom 1) <my-custom the-elements-attr-name="ParentModel + ' ---> adding more'"></my-custom><br/>
my-custom 2) <my-custom the-elements-attr-name="ParentNum1 + 12"></my-custom><br/>
</div>
my-custom 3) <my-custom the-elements-attr-name="ParentNum2 + 12"></my-custom><br/>
</body>
.... output
my-custom 1) the parents value ---> adding more
my-custom 2) 112
my-custom 3) 212
the '='
,Sync with existing model.
http://ngtutorial.com/learn/directive.html#/sync-existing
angular.module("myApp", []).directive("myCustom", function(){
return {
restrict: 'EA',
scope: {
myLocalModel: '=theElementsAttrName',
},
template: "{{ myLocalModel }}"
};
});
.....
<body ng-app="myApp">
<div ng-init="ParentModel='the parents value';
ParentNum1=100;
ParentNum2=200"></div>
<div ng-controller="CreateChildScopeController">
my-custom 1) <my-custom the-elements-attr-name="ParentModel"></my-custom><br/>
my-custom 2) <my-custom the-elements-attr-name="ParentNum1"></my-custom><br/>
</div>
my-custom 3) <my-custom the-elements-attr-name="ParentNum2"></my-custom><br/>
</body>
..... output
my-custom 1) the parents value
my-custom 2) 100
my-custom 3) 200

Related

Build template string inside directive

I'm trying to build a string of HTML code inside the directive and then use that as the directive's template.
I tried this but it doesn't work.
myApp.directive('myDirective', [function() {
return {
restrict: 'E',
scope: {
data: '=' // this is the data that will shape the HTML
},
template: str,
controller: function($scope){
$scope.str = ****BUILD THE STRING HERE USING 'data'****
}
};
}]);
Then I tried passing the app's scope to the directive, but got an error again
myApp.directive('myDirective', ['$scope', function($scope) {
var str = ****BUILD THE STRING HERE USING $scope.data****
return {
restrict: 'E',
template: str
};
}]);
I'm confused about how I can do this. In the first example the string is built correctly and when I do
template: {{str}}
I see the string but obviously, it just shows up as text on the page and it's not interpreted as HTML code.
Is there a way to access either myApp's or the directive controller's scope within the return statement of the directive?
I could build the string outside of the directive, but even if I do that I still can't access myApp's scope within the directive.
Directives, by nature have access to the outer scope, if you don't strictly define it with an inner scope (or isolated scope). Example:
angular.module('app',[]).controller('ctrl', function($scope) {
$scope.example = {
message: "Hello world"
};
}).directive("myDirective", function() {
return {
template: '<strong>{{example.message}}</strong>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div my-directive></div>
</div>
As you can see in the example above - The directive "knows" the outer scope values without you need to actually inject it.
Now, you can create an isolated scope and by doing this you don't limit yourself to a given scope:
angular.module('app',['ngSanitize']).controller('ctrl', function($scope) {
$scope.example = {
html: "<strong>Hello world</strong>"
};
}).directive("myDirective", function() {
return {
scope: {
data: '='
},
template: '<div ng-bind-html="data"></div>'
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular-sanitize.min.js"></script>
<div ng-app="app" ng-controller="ctrl">
<div my-directive data="example.html"></div>
</div>
By including ngSanitize I an able to use the ngBindHtml directive and pass an HTML structure to the directive inner scope.
Strictly speaking what you're trying to do can be achieved by using ng-if to output html that uses different directives based on the data count in your controller. This is better 'separation of concerns', as it means you're moving your presentation logic into the view where it belongs, and your controller is then concerned with your business logic including a data count variable (which you can then use in the ng-if).
If using this approach, you'll want to put your data retrieval into a service that the controller uses, and use the same controller for both directives.

Passing keys of object to directive

I have created a directive as a wrapper for md-autocomplete so that it's easier to re-use. In the parent controller, I have an object. I want to pass the keys of the object to my custom directive, but I'm having trouble. Simplified code, without md-autocomplete:
Here's the script
var app = angular.module('myApp',[])
.controller('parentController', function(){
var parent = this;
parent.things = {item1: {color: "blue"}, item2: {color: "red"}};
})
.directive('childDirective',function(){
return {
scope: {},
bindToController: {
items:'&'
},
controller: childController,
controllerAs: 'child',
template: '<pre>{{child.items | JSON}}<pre>' //should be [item1,item1]
}
function childController(){
//Just a dummy controller for now
}
})
HTML
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="Object.keys(parent.things)">
</my-directive>
</div>
TL;DR: How do I pass the keys of an object defined in the parent controller to a child directive? I need to pass just the keys, not the object itself, because my directive is designed to deal with an array of strings.
Try using a directive with local scope from user attribute (=)
app.directive('childDirective', function() {
return {
replace: true,
restrict: 'E',
scope: {
items: '='
},
template: '<pre>{{items | JSON}}<pre>'
};
});
Using the directive, object in attribute "items" is passed "as is" , as a scope variable "items"
<div ng-app="myApp" ng-controller="parentController as parent">
<my-directive items="getKeys(parent.things)">
</my-directive>
</div>
Using Object.keys(obj) as source will cause an infinite loop digest (the function is always returning a new different object). You need a function to save the result to a local updatable object, like in this example:
https://jsfiddle.net/FranIg/3ut4h5qm/3/
$scope.getKeys=function(obj){
//initialize result
this.result?this.result.length=0:this.result=[];
//fill result
var result=this.result;
Object.keys(obj).forEach(function(item){
result.push(item);
})
return result;
}
I'm marking #Igor's answer as correct, because ultimately it led me to the right place. However, I wanted to provide my final solution, which is too big for a comment.
The search for the answer to this question led me to create a directive that is more flexible, and can take several different types of input.
The real key (and my actual answer to the original question) was to bind the items parameter to a proxy getter/setter object in the directive. The basic setup is:
app.directive('myDirective',function(){
return {
...
controller: localControl,
bindToController: {
items: '<' //note one-way binding
}
...
}
function localControl(){
var child = this;
child._items = [],
Object.defineProperties(child,{
items: {
get: function(){return child._items},
set: function(x){child._items = Object.keys(x)}
}
});
}
});
HTML
<my-directive items="parent.items">
<!-- where parent.items is {item1:{}, item2:{}...} -->
</my-directive>
Ultimately, I decided I wanted my directive to be able to accept a variety of formats, and came up with this plunk as a demonstration.
Please feel free to offer comments/suggestions on improving my code. Thanks!

Angular scope function parameter return undefined

I'm calling a controller function from directive but the function parameter returns undefined when I console.log to check the value. Wondering what I'm doing wrong or maybe a step I forgot. I actually hard coded a value to see if this shows but only get undefined in the console. NOTE: The custom directive template is coming from external file so the function parameter is not being past to the controller. It only works if the custom directive element has the value attached. Should work with the inside directive html.
//******************** Directive ********************//
app.directive('customdir', [function() {
return {
restrict: "E",
template : "<div>Get product<button ng-click="addToCart(56)">Add to Cart</button></div>",
scope: {
addToCart:"&"
},
link: function(scope, el, attrs) {
}
};
}]);
//******************** Controller ********************//
app.controller('mycontroller', function($scope) {
$scope.addToCart = function(thumbProductId){
$scope.thumbProductId = thumbProductId;
console.log("thumbProductId =" + $scope.thumbProductId); // Returns Undefined
};
});
//******************** Html ********************//
<html>
<div ng-controller="mycontroller">
<custom-dir add-to-cart="addToCart(thumbProductId)"> </custom-dir>
</div>
</html>
There were a couple things wrong in the code, the first being "customdir" not having a "-" between it, as there's no capital. You also need to escape certain characters in your html, such as quotations and forward slashes. Here's a plunkr of your example:
http://plnkr.co/edit/FYUGBfIPtrl6Q7GWd597?p=preview
And the directive now looks:
myApp.directive('customdir', [function() {
return {
restrict: "E",
template : "<button ng-click=\"addToCart(thumbProductId)\">Add to Cart<\/button>",
scope: {
addToCart: "&"
}
};
}]);
Exposing Local Values to Parent Scope with Directive Expression Binding
To use Expression Binding to pass data from directive scope to parent scope, invoke the expression with a locals object.
myApp.directive('customdir', function() {
return {
restrict: "E",
template : "<button ng-click='addToCart({$id: 56})'>Add 56 to Cart</button>",
scope: {
addToCart: "&"
}
};
});
The above directive invokes the Angular Expression defined by the add-to-cart attribute with the value 56 exposed as $id.
The HTML:
<div ng-controller="mycontroller">
<customdir add-to-cart="thumbProductId = $id"> </customdir>
ThumbProductId => {{thumbProductId}}
</div>
When the user clicks on the button in the customdir element, the Angular Expression invoked will set thumbProductId to the value exposed by $id which in this case is 56.
To invoke a parent function with a local, simply make the Angular Expression a function:
<customdir add-to-cart="parentFn($id)"> </customdir>
The DEMO on PLNKR.

Using a function created in parent scope and also local scope varibles in an AngularJS directive with a template

So I am trying to create a directive which will replace an element with HTML which includes an ng-click which calls a function created in $scope.
An example I made can be found at http://jsfiddle.net/adinas/wbfu9ox3/5/
My HTML is
<div ng-app="myapp" ng-controller="mycontroller">
{{greeting}}
<!--This works-->
<div xyz></div>
<!--This works-->
<div ng-click="dosomething('Click me 2')">Click me 2</div>
<!--The click added in the directive here does NOT work-->
<div abc mytext="Click me 3"></div>
My JS is
var myapp = angular.module('myapp',[]);
myapp.controller('mycontroller', ['$scope', function($scope) {
$scope.greeting = 'Hola!';
$scope.dosomething = function (what) {
alert(what);
};
}]);
//Works
myapp.directive('xyz', function () {
return {
template: '<div ng-click="dosomething(\'Click me 1\')">Click me 1</div>'
};
});
//Does not work
myapp.directive('abc', function () {
return {
template: '<div ng-click="dosomething(\''+attr.mytext+'\')">Click me 3</div>'
};
});
I made 3 elements. The first two show that A. without the directive the click work. B. a directive without using the 'dosomething' function also works.
The 3rd element which tries to pass a parameter to the 'abc' directive and also call the function fails.
How can I also pass a parameter and also use the function? Thanks.
Well, if you look in the console, you will see an error that shows why this is not working:
Error: attr is not defined
You're trying to access an attr variable that doesn't exist.
The way to include attribute values in your directive template is to include them in the directive's scope:
scope: {
mytext: '#'
}
This means that there will now be a mytext variable in the scope of the directive.
This creates a problem, however, since the use of the scope: {} option creates an isolate scope for the directive, and this means that the enclosing controller's scope is not part of the directive's scope.
If feasible, the clean approach here is to also pass in the dosomething function as a parameter of the directive:
<div abc mytext="Click me 3" action='dosomething'></div>
return {
template: '<div ng-click="action(mytext)">Click me 3</div>',
scope: {
mytext: '#',
action: '='
}
};
http://jsfiddle.net/jx1hgjnr/1/
But if that's not what you want to do and you really want to access dosomething from the parent scope, one thing you can do is use $parent.dosomething:
return {
template: '<div ng-click="$parent.dosomething(mytext)">Click me 3</div>',
scope: {
mytext: '#'
}
};
http://jsfiddle.net/c815sqpn/1/
Such directive should have isolated scope, so it can be used everywhere.
That is simple directive that introduce two parameters: func and param.
app.directive('pit', function () {
return {
scope : {
func: '=',
param: '='
},
template: '<button ng-click="func(param)">Click me 3</button>'
};
});
And this is how u use it in html:
<div pit func="test1" param="test2"></div>
My plunker:
http://plnkr.co/edit/YiEWchRPwX6W7bohV3Zo?p=preview

Directive's isolated scope variables are undefined if it is wrapped in a directive which has in its template ng-if and tranclusion enabled

I am facing is an issue which is demonstrated in the following example:
http://jsfiddle.net/nanmark/xM92P/
<div ng-controller="DemoCtrl">
Hello, {{name}}!
<div my-wrapper-directive>
<div my-nested-directive nested-data="nameForNestedDirective"></div>
</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('DemoCtrl',['$scope', function($scope){
$scope.name = 'nadia';
$scope.nameForNestedDirective = 'eirini';
}])
.directive('myWrapperDirective', function(){
return {
restrict: 'A',
scope: {},
transclude: true,
template: "<div ng-if='isEnabled'>Hello, <div ng-transclude></div></div>",
link: function(scope, element){
scope.isEnabled = true;
}
}
})
.directive('myNestedDirective', function(){
return {
restrict: 'A',
scope: {
nestedData: '='
},
template: '<div>{{nestedData}}</div>',
};
});
I want to create a directive (myWrapperDirective) which will wrap many other directives such as 'myNestedDirective of my example.
'myWrapperDirective' should decide if its content will be displayed or not according to ng-if expression's value, but if contents is a directive like 'myNestedDirective' with an isolated scope then scope variable 'nestedData' of 'myNestedDirective' is undefined.
The problem is with the double-nested isolated scopes. You see, you are using the nameForNestedDirective variable, defined in the outer scope from the inner scope which is isolated. This means it does not inherit this variable, thus undefined is passed to the nested directive.
A diagram to explain:
Outer scope - DemoCtrl
- Defines: name
- Defines: nameForNestedDirective
+ Uses: name
Inner isolated scope 1 - myWrapperDirective
- Defines: (nothing)
- Inherits: (NOTHING! - It is isolated)
+ Uses: (nothing)
* Passes nestedData=nameForNestedDirective to nested directive, but
nameForNestedDirective is undefined here!
Inner isolated scope 2 - myNestedDirective
- Defines: nestedData (from scope definition)
- Inherits: (NOTHING! - It is isolated)
+ Uses nestedData
You can convince yourself this is the case by commenting out the scope definition of the wrapper directive ("hello eirini" is displayed as expected):
.directive('myWrapperDirective', function(){
return {
...
//scope: {},
...
I am not sure if the wrapper directive really needs to have an isolated scope. If it doesn't, maybe removing the isolated scope will solve your problem. Otherwise you will have either to:
Pass the data first to the wrapper and then to the nested directives
Pass the data to the wrapper directive, write a controller for it that exposes the data and then require the wrapper controller from the nested directive.

Resources