Passing scope variables to an AngularJS controller using fat arrows - angularjs

I'm updating an AngularJS application to use fat arrow syntax for anonymous functions. I know that I need to use version 1.5, but some things still don't work. For example, here I have a custom directive that passes the string 'hello' to its controller, which then outputs the string as an alert:
<div data-my-directive="hello">Text that will be replaced by my-template</div>
angular
.module('myModule')
.directive('myDirective', () => (
{
restrict: 'A',
templateUrl: 'my-template',
controller: 'myController',
controllerAs: 'myCtrl',
bindToController: true,
scope: {myVariable : '#myDirective'}
}))
.controller('myController', () => {
alert('the scope variable is: ' + this.myVariable );
}
But this alerts 'the scope variable is: undefined'.
If I change the controller definition to use ES5 syntax, then it alerts 'the scope variable is: hello', as expected.
.controller('myController', function() {
alert('the scope variable is: ' + this.myVariable);
}
I guess that this is something to do with the binding of this.
Is there a way to use the fat arrow notation when passing scope variables as above?

In this case, you have to use function instead of ()=>.
If you do :
.controller('myController', () => {
console.log(this); // -> window
})
If you use arrow function here, this === window. A controller need a real scope to work properly. I am pretty sure you doesn't want window as common object for all controllers in your app :)
Arrow function is really useful with class and callback but should not be use every time.

Angular calls the controller function like this: fn.apply(self, args);
where self (which becomes this in the invoked function) is an object that has the required fields - i.e. myVariable in the example.
Arrow functions ignore the first of apply's arguments. So as #Yoann says, we must use function() {...} rather than () => {...}.

Related

ng-repeat overrides property in function but not in view model

I am new to angular and have a problem using my custom directive with ng-repeat. I want to display some posts I get from a rest interface and then use their _id property inside the directive for other purposes. However, it turns out that the property is always the one from the last displayed post when used from inside a function (test in the sample below). When trying to display the id directly over the viewmodel it shows the right one. Hope this makes sense. Any help would be appreciated.
//directive.js
angular.module('app').directive('gnPost', myGnPost);
function myGnPost() {
return {
restrict: 'E',
controller: 'postCtrl',
controllerAs: 'postCtrl',
bindToController: {
post: '='
},
scope: {},
templateUrl: 'template.html'
};
};
//controller.js
angular.module('app').controller('postCtrl', myPostCtrl);
function myPostCtrl(postRestService) {
vm = this;
vm.test = function () {
return vm.post._id;
};
};
// template.html
<p>{{postCtrl.post._id}}</p>
//displays the right id
<p>{{postCtrl.test()}}</p>
//displays the id of the last element of ng-repeat
//parent page.html
<gn-post ng-repeat="singlePost in posts.postList" post="singlePost"></gn-post>
In your controller, you have the following line:
vm = this;
It should be:
var vm = this;
By omitting the var, there is a vm variable created on the global scope instead of a local one per controller instance. As a result each iteration when vm.test is called, it's always pointing at the function defined on the last controller.
Fiddle - try including/omitting the var in postCtrl
It's good practice to use strict mode in Javascript to prevent that issue and others. In strict mode, it impossible to accidentally create global variables, as doing so will throw an error and you'll see the problem straight away. You just need to add this line at the start of your file or function:
"use strict";

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: Multiple ways to pass function from controller to directive

I am trying to write component-style AngularJS, similar to the practice put forward by this article.
However, I have come to realize there are various ways to pass functions to directives from an associated controller. The directive I'm working on is quite complex and I was passing each function in by binding to the directive in the template, but I now see I could just implicitly inherit the $scope object or reference the Controller object directly.
Here is an example of what I mean:
app.js
var app = angular.module('plunker', [])
app
.controller('myCtrl', function($scope) {
$scope.output = '';
// fn foo is passed into directive as an argument
$scope.foo = function () {
$scope.output = 'foo';
}
// fn inherited from controller
$scope.bar = function () {
$scope.output = 'bar';
}
// fn attached to ctrl object and referenced directly
this.baz = function () {
$scope.output = 'baz';
}
})
.directive('myDirective', function() {
return {
scope: {
output: '=',
foo: '&',
},
templateUrl: 'template.html',
replace: true,
controller: 'myCtrl',
controllerAs: 'ctrl'
};
})
index.html
<body ng-controller="myCtrl">
<my-directive
output="output"
foo="foo()">
</my-directive>
</body>
template.html
<div>
<button ng-click="foo()">Click Foo</button>
<button ng-click="bar()">Click Bar</button>
<button ng-click="ctrl.baz()">Click Baz</button>
<p>You clicked: <span style="color:red">{{output}}</span></p>
</div>
Plunkr: http://plnkr.co/edit/1JzakaxL3D2L6wpPXz3v?p=preview
So there are three functions here and they all work, yet are passed to the directive in different ways. My question is what are the merits of each and which is the best from a code and testability perspective?
You're not really passing anything to the directive, as it's using the same controller as the file containing it...
For instance, if you delete the following:
scope: {
output: '=',
foo: '&',
}
from your directive, everything still works the same. I can't think of a reason to use the same controller for a directive and and the containing application like this. I would never recommend this approach.
If you also remove
controller: 'myCtrl',
controllerAs: 'ctrl'
only foo and bar work. This is because the directive inherits the scope it's contained in. This is only recommended if your directive is pretty simple and tightly coupled to the view using it. Usually this approach is OK when you're just doing some visual modifications that repeat themselves in the page. Just notice that when you change something in the controller, the directive will probably break, and that goes against the encapsulation principle.
Finally, the correct way to pass a function to a directive is indeed using '&' modifier. This lets your directive keep an isolated scope, which means it won't break if some code on the containing controller changes. This makes your directive truly an encapsulated, independent module that you can "drag and drop" anywhere.
Here's a fork of your plunkr.

Call method on Directive to pass data to Controller

So basically I have a controller, which lists a bunch of items.
Each item is rendering a directive.
Each directive has the ability to make a selection.
What I want to achieve is once the selection has been made, I want to call a method on the controller to pass in the selection.
What I have so far is along the lines of...
app.directive('searchFilterLookup', ['SearchFilterService', function (SearchFilterService) {
return {
restrict: 'A',
templateUrl: '/Areas/Library/Content/js/views/search-filter-lookup.html',
replace: true,
scope: {
model: '=',
setCriteria: '&'
},
controller: function($scope) {
$scope.showOptions = false;
$scope.selection = [];
$scope.options = [];
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
};
}]);
The directive is used like this:
<div search-filter-lookup model="customField" criteria="updateCriteria(criteria)"></div>
Then the controller has a function defined:
$scope.updateCriteria = function(criteria) {
console.log("Weeeee");
console.log(criteria);
};
The function gets called fine. But I'm unable to pass data to it :(
Try this:
$scope.setCriteria({criteria: option});
When you declare an isolated scope "&" property, angular parses the expression to a function that would be evaluated against the parent scope.
when invoking this function you can pass a locals object which extends the parent scope.
It's a common mistake to think that $scope.setCriteria is the same as the function inside the attribute. If you log it you'll see it's just an angular parsed expression function which have the parent scope saved at it's closure.
So when you run $scope.setCriteria() you actually evaluate an expression against the parent scope.
In your case this expression happens to be a function but it could be any expression.
But you don't have a criteria property on the parent scope, that's why angular let you pass a locals object to extend the parent scope. e.g. {criteria: option}
Extends the parent scope
you wrote in a comment that it requires the directive to have knowledge of the parameter name defined in the controller. No it doesn't, it just extends the parent scope with a criteria option, you can still use any expression you want though you are provided with an extra property you may use.
A good example would be ngEvents, take ng-click="doSomething($event)":
ngClick provides you with a local property $event, you don't have to use but you may if you need.
the directive doesn't know anything about the controller, it's up to you to decide which expression you write, cheers.
You can pass the function in using =...
scope: {
model: '=',
setCriteria: '='
},
controller: function($scope) {
// ...
$scope.selectOption = function(option) {
$scope.selection.push(option);
$scope.setCriteria(option);
};
}
<div search-filter-lookup model="customField" criteria="updateCriteria"></div>

Access controller scope from directive

I've created a simple directive that displays sort column headers for a <table> I'm creating.
ngGrid.directive("sortColumn", function() {
return {
restrict: "E",
replace: true,
transclude: true,
scope: {
sortby: "#",
onsort: "="
},
template: "<span><a href='#' ng-click='sort()' ng-transclude></a></span>",
link: function(scope, element, attrs) {
scope.sort = function () {
// I want to call CONTROLLER.onSort here, but how do I access the controller scope?...
scope.controllerOnSort(scope.sortby);
};
}
};
});
Here's an example of some table headers being created:
<table id="mainGrid" ng-controller="GridCtrl>
<thead>
<tr>
<th><sort-column sortby="Name">Name</sort-column></th>
<th><sort-column sortby="DateCreated">Date Created</sort-column></th>
<th>Hi</th>
</tr>
</thead>
So when the sort column is clicked I want to fire the onControllerSort function on my grid controller.. but I'm stuck! So far the only way I've been able to do this is for each <sort-column>, add attributes for the "onSort" and reference those in the directive:
<sort-column onSort="controllerOnSort" sortby="Name">Name</sort-column>
But that's not very nice since I ALWAYS want to call controllerOnSort, so plumbing it in for every directive is a bit ugly. How can I do this within the directive without requiring unnecesary markup in my HTML? Both the directive and controller are defined within the same module if that helps.
Create a second directive as a wrapper:
ngGrid.directive("columnwrapper", function() {
return {
restrict: "E",
scope: {
onsort: '='
}
};
});
Then you can just reference the function to call once in the outer directive:
<columnwrapper onsort="controllerOnSort">
<sort-column sortby="Name">Name</sort-column>
<sort-column sortby="DateCreated">Date Created</sort-column>
</columnwrapper>
In the "sortColumn" directive you can then call that referenced function by calling
scope.$parent.onsort();
See this fiddle for a working example: http://jsfiddle.net/wZrjQ/1/
Of course if you don't care about having hardcoded dependencies, you could also stay with one directive and just call the function on the parent scope (that would then be the controller in question) through
scope.$parent.controllerOnSort():
I have another fiddle showing this: http://jsfiddle.net/wZrjQ/2
This solution would have the same effect (with the same criticism in regard to hard-coupling) as the solution in the other answer (https://stackoverflow.com/a/19385937/2572897) but is at least somewhat easier than that solution. If you couple hard anyway, i don't think there is a point in referencing the controller as it would most likely be available at $scope.$parent all the time (but beware of other elements setting up a scope).
I would go for the first solution, though. It adds some little markup but solves the problem and maintains a clean separation. Also you could be sure that $scope.$parent matches the outer directive if you use the second directive as a direct wrapper.
The & local scope property allows the consumer of a directive to pass in a function that the directive can invoke.
See details here.
Here is a answer to a similar question, which shows how to pass argument in the callback function from the directive code.
In your directive require the ngController and modify the link function as:
ngGrid.directive("sortColumn", function() {
return {
...
require: "ngController",
...
link: function(scope, element, attrs, ngCtrl) {
...
}
};
});
What you get as ngCtrl is your controller, GridCtrl. You dont get its scope though; you would have to do something in the lines of:
xxxx.controller("GridCtrl", function($scope, ...) {
// add stuff to scope as usual
$scope.xxxx = yyyy;
// Define controller public API
// NOTE: USING this NOT $scope
this.controllerOnSort = function(...) { ... };
});
Call it from the link function simply as:
ngCtrl.controllerOnSort(...);
Do note that this require will get the first parent ngController. If there is another controller specified between GridCtrl and the directive, you will get that one.
A fiddle that demonstrates the principle (a directive accessing a parent ng-controller with methods): http://jsfiddle.net/NAfm5/1/
People fear that this solution may introduce unwanted tight coupling. If this is indeed a concern, it can be addressed as:
Create a directive that will be side-by-side with the controller, lets call it master:
<table id="mainGrid" ng-controller="GridCtrl" master="controllerOnSort()">
This directive references the desired method of the controller (thus: decoupling).
The child directive (sort-column in your case) requires the master directive:
require: "^master"
Using the $parse service the specified method can be called from a member method of the master controller. See updated fiddle implementing this principle: http://jsfiddle.net/NAfm5/3/
There is another way to do this, although given my relative lack of experience I can't speak for the fitness of such a solution. I will pass it along anyhow just for informational purposes.
In your column, you create a scope variable attribute:
<sort-column data-sortby="sortby">Date Created</sort-column>
Then in your controller you define the scope variable:
$scope.sortby = 'DateCreated' // just a default sort here
Then add your sort function in controller:
$scope.onSort = function(val) {
$scope.sortby = val;
}
Then in your markup wire up ng-click:
<sort-column data-sortby="sortby" ng-click="onSort('DateCreated')">Date Created</sort-column>
Then in your directive you add the sortby attribute to directive scope:
scope: {
sortby: '=' // not sure if you need
}
And in your "link:" function add a $watch:
scope.$watch('sortby', function () {
... your sort logic here ...
}
The beauty of this approach IMO is that your directive is decoupled completely, you don't need to call back to onSort from the directive because you don't really leave onSort in the controller during that part of the execution path.
If you needed to tell your controller to wait for the sort to finish you could define an event in the controller:
$scope.$on("_sortFinished", function(event, message){
..do something...
});
Then in your directive simply emit the event then the process is done:
$scope.$emit('_sortFinished');
There's other ways to do that, and this kind of adds some tight-ish coupling because your controller has to listen for. and your directive has to emit a specific even... but that may not be an issue for you since they are closely related anyhow.
Call me crazy, but it seems easier to just get the controller from the element via the inbuilt method for that, rather than fiddling with require:
var mod = angular.module('something', []).directive('myDir',
function () {
return {
link: function (scope, element) {
console.log(element.controller('myDir'));
},
controller: function () {
this.works = function () {};
},
scope: {}
}
}
);
http://plnkr.co/edit/gY4rP0?p=preview

Resources