Difference between & and = for passing functions to isolate scope - angularjs

& is always described as the way to call a function on the parent scope inside a directive's isolated scope.
However, since = creates two-way binding and a function is just another value, shouldn't it be just as effective for this purpose?
The only difference I see is that using &, you can modify the passed function without affecting the parent, since it's one-way binding.
So why is & usually recommended over = for this use case?
There is also some weird behavior that I've come across. Using & gives you a function wrapper. If you unwrap it in the controller and call it, it will resolve differently than if you unwrap it as the result of an ng-click inside the directive.
I've set up an experiment in this fiddle:
app.directive('myDir', function() {
return {
restrict: 'E',
template: '<button ng-click="parentFunc1()(1); parentFunc2(1)">Directive</button>',
scope: {
parentFunc1: '&func1',
parentFunc2: '=func2',
},
controller: Ctrl2,
}
});
function Ctrl2($scope) {
//Step 1
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 2
$scope.oldParent1 = $scope.parentFunc1;
$scope.parentFunc1 = function (value) {
console.log(value+1);
};
$scope.parentFunc1(1);
$scope.parentFunc2(1);
//Step 3
$scope.parentFunc1 = $scope.oldParent1;
$scope.parentFunc2 = function (value) {
console.log(value+2);
};
console.log($scope.parentFunc1);
$scope.parentFunc1()(1);
$scope.parentFunc2(1);
//Step 4 -> Click the directive button
}
function Ctrl($scope){
$scope.foo = function (value) {
console.log(value);
};
}
This logs "1,1; 2,1; 1,2; 2,2". The last two pairs of values leave me puzzled because they seem to execute the same code.

Very good question!
See the difference between & and = is simple.
When you are declaring a directive scope, and you add to it & it means that you are declaring a function within the scope rather if it was = it was for a regular property.
WAIT WAIT, those two examples above just worked and they are both functions!
Well that's true but hold on,
You just used them incorrectly.
Using the :"&func" means that you are adding a function that will be evaluated soon.
Confused?
I'll type a perfect example:
<script type="text/javascript">
angular.module("exampleApp", [])
.directive("scopeDemo", function (){
return {
template: "<div><p>Name: {{local}}, City: {{cityFn()}}</p></div>",
scope:{
local: "=nameprop",
cityFn: "&city"
}
}
}
}).controller("scopeCtrl, function($scope){
$scope.data = {
name: "Shahar",
defaultCity: "London"
};
$scope.getCity = function(name){
return name == 'Shahar' ? $scope.data.defaultCity : "unknown";
}
});
</script>
<body ng-controller="scopeCtrl">
<div>
Direct Binding: <input ng-model="data.name" />
</div>
<div scope-demo city="getCity(data.name)" nameprop="data.name"></div> //Reference 1.
</body>
As you can see I've written two attributes to my scope's directive.
one accepts a PROPERTY and one accepts a FUNCTION.
As you can see the result of this directive is rather dull, but it explains the whole point.
You will not succeed doing so if you try to make a function with the '=' since Angular will just ignore that.
I hope it clears it up.
Good luck!

The difference between & and = binding strategies takes place when you want to call function on parent scope with parameters also passed from parent scope.
Let's say you have following controller:
angular.module('myApp').controller('myCtrl', function() {
$scope.mans = [{name: 'Peter'}, {name: 'Alex'}]
$scope.someMethod = function(par) {
console.log(par);
}
});
And HTML:
<div ng-repeat="man in mans">
<button my-dir="someMethod(man.name)">Click me</button>
</div>
In this case myDir directive should only use & binding strategy, because the directive knows nothing aboout passed parameters.

Related

Inserting a function into directive without isolate scope

I'm not sure if I'm going about this the right way. I am using the ui-select directive which does not seem to support the HTML required directive. So I built my own, ui-select-required. It seems I am unable to use isolate scope because ui-select already instantiates an isolate scope.
I want to make ui-select-required take in a function as an attribute. If the attribute is present, the it should validate with the return value of this function. If the attribute is not present then it should validate on presence of a value. This is all a part of a component.
product_details.js
angular
.module('ProductComponents')
.component('productDetails', {
bindings:{
product: '=product',
},
templateUrl: "/template/admin/products/details",
controllerAs: 'prodDetails',
controller: [
'v3Stitcher',
'AjaxLoaderSvc',
'ModelInformationSvc',
'$filter',
'$http',
'current_site',
function(
v3Stitcher,
AjaxLoaderSvc,
ModelInformationSvc,
$filter,
$http,
current_site
){
var prodDetails = this;
...
prodDetails.templateRequired = function(){
// Product types requiring a template
// 3 - customizable_downloadable
// 6 - static_variable_downloadable
var productTypes = [3, 6];
// Specification types requiring a template
var specificationTypes = ["print_on_demand"];
if(productTypes.indexOf(prodDetails.product.product_type) > -1){
return true;
}
if(specificationTypes.indexOf(prodDetails.specification.specification_type) > -1){
console.log('here'); // this gets called
return true;
}
return false;
};
.directive('uiSelectRequired',function(){
return {
restrict:'A',
require:'ngModel',
link:function(scope, elem, attrs, ctrl){
var form = angular.element(document).find('form');
var input = angular.element(elem.find('input')[0]);
var requiredFn = scope[attrs['requiredFn']];
if(requiredFn){
ctrl.$validators.uiSelectRequired = function(){
return requiredFn();
};
} else {
ctrl.$validators.uiSelectRequired = function(modelValue){
return !ctrl.$isEmpty(modelValue)
};
}
form.on('submit', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-valid').addClass('ng-invalid');
}
});
elem.on('change', function(){
if(ctrl.$invalid){
elem.find('span').removeClass('ng-invalid').addClass('ng-valid');
}
});
}
};
});
details.slim
label(ng-class="{'label label-danger': prodDetails.templateRequired()}")
| Template
ui-select(ng-model="prodDetails.product.template_id" name="template" ng-model-options="{ debounce: { default:500, blur: 0 } }" ui-select-required required-fn="prodDetails.templateRequired")
ui-select-match(placeholder="Search Templates...")
| {{$select.selected.name}}
ui-select-choices(position="down" repeat="template.id as template in prodDetails.templates" refresh="prodDetails.refreshTemplates($select.search)" minimum-input-length="1" refresh-delay="0")
| {{ template.name }}
br
| id: {{template.id}}
br
| created: {{template.created_at | date : 'yyyy-MM-dd'}}
The problem I'm having is that the variable requireFn is undefined. However, if in the HTML I send in the controller variable prodDetails alone then requireFn has the correct value of the controller variable.
I think your problem is that:
You are doing controllerAs: 'prodDetails' in your isolate scope and
You are looking to reference the function directly on the scope in your uiSelectRequired directive
I think if you switch this:
var requiredFn = scope[attrs['requiredFn']];
to:
var requiredFn = scope.$eval(attrs.requiredFn);
You should get what you are looking for. This is assuming that templateRequired property has been added to the productDetails component's controller instance.
To reiterate, your issue was that you were looking for the property directly on the isolate scope itself, where it has been added to the controller reference. By doing a scope.$eval, you will essentially be parsing the path prodDetails.templateRequired -- which will hopefully resolve to the function reference you were hoping to get in the first place.
Edit: So the second part of your question in the comments lead me to believe you never needed a function into a directive with isolate scope. I think what you are trying to do is make the template model required conditionally. Angular already gives you this functionality through required and ng-required directives. You state in your question these are not available on ui-select, but they are "helper" directives with ngModel. I believe this is a mostly working example of what you want to do where I switch to required/ng-required and eliminate the need for your custom directive.

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!

Call to function from directive to controller using "&" doesn't send the parameters

I have a directive that calls a function in my controller using "&". The invocation works and my function gets executed but the parameters are not send to the function and i get 'undefined'.
The directive:
angular.module("tiki").directive("inputFile", function(){
return {
restrict:"A",
scope:{
onFile: "&",
},
controller:function($scope, $element, $attrs){
var inputFile = $element.find("input")[0]
$element[0].addEventListener("click", function(){
inputFile.click()
})
inputFile.addEventListener("change", function(e){
var file = inputFile.files[0]
var reader = new FileReader()
reader.addEventListener("load", function(){
$scope.onFile(reader.result)
})
reader.readAsDataURL(file)
})
}
}
})
The controller:
angular.module('tiki').controller("tiki.controller.settings.edit", ["$scope", "editTiki", function($scope, editTiki){
$scope.onFile = function(file){
console.log(file)
}
}])
The HTML
<li input-file on-file="onFile()"><input type="file" style="display:none;"></li>
& is often misunderstood because the primary examples in the angular doc's reference passing functions. However, what & actually does is add a function to the directive scope that, when called, evaluates the expression against the directive's parent scope and returns the result. The expression does not have to be a function. It can be any valid Angular expression.
It is a way to implement one-way binding.
So
on-file="onFile(fileObj)"
creates this function (note - this is simplified. The actual implementation is optimized such that $parse isn't called for each invocation of the function):
scope.onFile = function(locals) {
return $parse(attrs.onFile)(scope.$parent, locals);
}
Note the "locals" parameter. This is what enables your second example
$scope.onFile({fileObj:reader.result})
{fileObj: reader.result} <---locals
When you pass locals to the function returned by $parse, angular replaces any references to those locals that it finds that would be normally attributed to the parent scope with the values in the locals map.
Note that if you didn't pass in locals, and instead just did:
$scope.onFile()
Then angular would run the parent scope's onFile function and assume that fileObj was either a property of the parent scope or a global object. If it's neither, it will be undefined in the function.
When you pass just:
on-file="onFile"
then the result of calling the directive's
scope.onFile()
method is the parent scope's onFile function, so to invoke the function we just need to pass it data:
scope.onFile() === scope.$parent.onFile
scope.onFile()(reader.result)
is equivalent to
scope.$parent.onFile(reader.result)
The right answer was actually this:
Option 1:
Send parameters as an object literal.
HTML:
<li input-file on-file="onFile(fileObj)"><input type="file" style="display:none;"></li>
Directive:
reader.addEventListener("load", function(){
$scope.onFile({fileObj:reader.result})
})
Option 2:
Return a function and invoke it with a parameter list
HTML:
<li input-file on-file="onFile"><input type="file" style="display:none;"></li>
Directive:
reader.addEventListener("load", function(){
$scope.onFile()(reader.result)
})
Some reading material:
https://gist.github.com/CMCDragonkai/6282750
http://weblogs.asp.net/dwahlin/creating-custom-angularjs-directives-part-3-isolate-scope-and-function-parameters

AngularJS: Parent scope is not updated in directive (with isolated scope) two way binding

I have a directive with isolated scope with a value with two way binding to the parent scope. I am calling a method that changes the value in the parent scope, but the change is not applied in my directive.(two way binding is not triggered). This question is very similar:
AngularJS: Parent scope not updated in directive (with isolated scope) two way binding
but I am not changing the value from the directive, but changing it only in the parent scope. I read the solution and in point five it is said:
The watch() created by the isolated scope checks whether it's value for the bi-directional binding is in sync with the parent's value. If it isn't the parent's value is copied to the isolated scope.
Which means that when my parent value is changed to 2, a watch is triggered. It checks whether parent value and directive value are the same - and if not it copies to directive value. Ok but my directive value is still 1 ... What am I missing ?
html :
<div data-ng-app="testApp">
<div data-ng-controller="testCtrl">
<strong>{{myValue}}</strong>
<span data-test-directive data-parent-item="myValue"
data-parent-update="update()"></span>
</div>
</div>
js:
var testApp = angular.module('testApp', []);
testApp.directive('testDirective', function ($timeout) {
return {
scope: {
key: '=parentItem',
parentUpdate: '&'
},
replace: true,
template:
'<button data-ng-click="lock()">Lock</button>' +
'</div>',
controller: function ($scope, $element, $attrs) {
$scope.lock = function () {
console.log('directive :', $scope.key);
$scope.parentUpdate();
//$timeout($scope.parentUpdate); // would work.
// expecting the value to be 2, but it is 1
console.log('directive :', $scope.key);
};
}
};
});
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
console.log('CTRL:', $scope.myValue);
};
});
Fiddle
Use $scope.$apply() after changing the $scope.myValue in your controller like:
testApp.controller('testCtrl', function ($scope) {
$scope.myValue = '1';
$scope.update = function () {
// Expecting local variable k, or $scope.pkey to have been
// updated by calls in the directive's scope.
console.log('CTRL:', $scope.myValue);
$scope.myValue = "2";
$scope.$apply();
console.log('CTRL:', $scope.myValue);
};
});
The answer Use $scope.$apply() is completely incorrect.
The only way that I have seen to update the scope in your directive is like this:
angular.module('app')
.directive('navbar', function () {
return {
templateUrl: '../../views/navbar.html',
replace: 'true',
restrict: 'E',
scope: {
email: '='
},
link: function (scope, elem, attrs) {
scope.$on('userLoggedIn', function (event, args) {
scope.email = args.email;
});
scope.$on('userLoggedOut', function (event) {
scope.email = false;
console.log(newValue);
});
}
}
});
and emitting your events in the controller like this:
$rootScope.$broadcast('userLoggedIn', user);
This feels like such a hack I hope the angular gurus can see this post and provide a better answer, but as it is the accepted answer does not even work and just gives the error $digest already in progress
Using $apply() like the accepted answer can cause all sorts of bugs and potential performance hits as well. Settings up broadcasts and whatnot is a lot of work for this. I found the simple workaround just to use the standard timeout to trigger the event in the next cycle (which will be immediately because of the timeout). Surround the parentUpdate() call like so:
$timeout(function() {
$scope.parentUpdate();
});
Works perfectly for me. (note: 0ms is the default timeout time when not specified)
One thing most people forget is that you can't just declare an isolated scope with the object notation and expect parent scope properties to be bound. These bindings only work if attributes have been declared through which the binding 'magic' works. See for more information:
https://umur.io/angularjs-directives-using-isolated-scope-with-attributes/
Instead of using $scope.$apply(), try using $scope.$applyAsync();

Function with args in parent scope is being binded but not being called in angular directive

I'm trying to call a function that is in the parent scope of a directive but it is not being called.
We want to pass the parameter 'arg' that is being modified to the parent scope and do some logic to it like 'arg = arg + 1', but this function is not being called.
function MyCtrl($scope) {
$scope.arg = 0
$scope.myFunction = function(arg) {
//do some logic like '+ 1'
arg++;
$scope.arg = arg;
}
}
myApp.directive('directive', function () {
return {
restrict: 'E',
scope: {
arg: '=arg',
fun: '&'
},
template: '<input ng-model="arg"/>',
link: function (scope) {
scope.$watch('arg', function (arg) {
scope.fun(arg);
});
}
};
});
Here is a little fiddle with a basic use case. http://jsfiddle.net/HB7LU/2677/
The $scope function isn't being called because you are parsing it wrong.
To parse a function you need to assign it like so:
<directive arg="arg" fun="myFunction()"></directive>
If you want to pass arguments to it, you need to be just a little more careful. First you need to describe the arguments in advance like so:
<directive arg="arg" fun="myFunction(arg)"></directive>
and when you're calling the function inside the directive you apply it like so:
scope.fun({arg: arg});
I created a jsFiddle that solves your issue: jsFiddle
Please note that your own fiddle had a recursive call in it: everytime you increase arg you also trigger the $watch and so it went into a loop. I changed that a bit (since I know you made it just to show your example.
If you want to learn a bit more about the & parsing in directives, check out this video which I highly recommend: egghead.io isolate scope &

Resources