What does passing `this` to a function inside ng-click expose? - angularjs

Assume we have a button with a ng-click directive.It calls a function passing this as an argument.
Upon inspection, the type of the parameter passed is found to be an object.But neither does it have any of the element properties , nor is it a jQuery selector.
What exactly gets passed ?

ngClick and similar directives are executed in context of the current scope. So this refers scope object.
If you don't have ngRepeat, ngInclude or other directive that creates new scope then you can check in controller function and verify that this ng-click="test(this)" will pass something as $scope:
$scope.test = function(something) {
console.log(something === $scope); // => true
}
Another example. With ngRepeat you get new child scope per iteration, so in this case if you have this list:
<li ng-repeat="n in numbers">
<button ng-click="test(this)">{{n}}</button>
</li>
you will have something being a child of the main $scope:
$scope.test = function(something) {
console.log(something.$parent === $scope); // => true
};
Demo: http://plnkr.co/edit/nxxCn7SYUA4PfJpJoueu?p=info

Related

Target ng-if outside controller

I have a feedback feature on my app,
#feedback
%h3.button
%a{"ui-sref" => ".feedback", "ng-click" => "feedbackActive=!feedbackActive;"}
Feedback
#feedback-container{"ng-if" => "feedbackActive"}
%div{"ui-view" => "feedback"}
The #feedback-container has a ng-if so that the content is only loaded when needed. The %a has a ng-click that toggles between true/false for the the feedbackActive state.
This works fine. Howoever in my ui-view I load a template. In that template is a send button that has a send() function linked to the feedbackCtrl,
$scope.send = function(){
console.log ('send')
console.log ($scope.feedbackForm)
createFeedback.create({
name: $scope.feedbackForm.name,
feedback: $scope.feedbackForm.feedback
})
$scope.feedbackActive = false;
}
It runs the code fine, but doesn't give the feedbackActive the false value so nothing happens.
How do remove toggle the ng-if from outside the controller?
This is a classic scoping issue. Your controller's scope is a child of the scope of the ng-if directive. One option is to use $scope.$parent to set the variable on the parent scope.
$scope.send = function(){
console.log ('send')
console.log ($scope.feedbackForm)
createFeedback.create({
name: $scope.feedbackForm.name,
feedback: $scope.feedbackForm.feedback
})
$scope.$parent.feedbackActive = false;
}
The other option is to take advantage of prototypical inheritance. In the parent controller, initialize an object.
$scope.x = {}; //parent scope
In the view controller, set properties on the inherited object.
$scope.x.feedbackActive = false; //child scope
And of course in your HTML
<feedback-container ng-if="x.feedbackActive">

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

Changing model value inside directive

I have a tag inside a directive, click on this anchor should change state of the directive's model variable and refresh the directive view that depends on this scope variable. There are two way to achieve this in Angular:
1)
<my-directive>
<a ng-click="myProperty = 'foo'">bar</a>
</my-directive>
Inside the directive controller:
$scope.$watch('myProperty', function(value) {
myPromise.then(function() { //the promise exists in real code but does not have anything to do with the question itself
updateComponent(value);
});
});
2)
<my-directive>
<a ng-click="handleMyPropertyChange('foo')">bar</a>
</my-directive>
Inside the directive controller:
$scope.handlePropertyChange = function(value) {
$scope.myProperty = value;
myPromise.then(function() {
updateComponent(value);
});
});
Which one is preferred and why?
I prefer the second option. Code belongs in the controller and even a simple assignment is still code.

Angular Directive Two-Way Binding Issue

I am creating a custom input directive for some added features.
app.directive('myTextInput', [
function () {
var directive = {};
directive.restrict = 'A';
directive.scope = {
textModel: '='
};
directive.link = function ($scope, element, attrs) {
// FUnctionality
};
directive.template = '<input ng-model="textModel" type="text">';
return directive;
}]);
and used the isolated scope property textModel for two way binding.
And the parent scope HTML is:
<div my-text-input text-model="myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
The controller scope has variable $scope.myScopeVariable=""; defined.
When i do this.
what ever typed in the directive input is able to print in the <span> tag.
Which tells it is updating.
The issue is.
in the parent scope. i have a method that will execute on a button click.
$scope.doSomething = function(){
console.log($scope.myScopeVariable);
};
On click on the button. the log is empty. which is supposed to be what is typed in the directive input.
THIS IS STRANGE
Now if define the scope variable as
$scope.myScopeVariable = {key:''};
and use in HTML as
<div my-text-input text-model="myScopeVariable.key"></div>
<span>TYPED : {{myScopeVariable.key}}</span>
then it is working every where. Even in the previously said function doSomething().
Any idea what happening here ?
This is happen because myScopeVariable contain value as String.
So if you change the value of from textbox of directive. It wont be reflected.
And in second method, you are referring Object. so whenever you change value of object , its relevant watch method is called. And value will be updated.
my-text-input is a directive, so he has his own scope, so the model myScopeVariable is not watch by the parent.
try with this :
<div my-text-input text-model="$parent.myScopeVariable"></div>
<span>TYPED : {{myScopeVariable}}</span>
It's not the best practice but it might works.

Angular ng-click event delegation

So if i have a ul with 100 li's should there be ng-clicks in each li or is there a way to bind the event to the ul and delegate it to the li's kind of what jquery does? Would this be better or worse? Are we having 100 events or is it just one event in the end?
It seems angular doesn't do event delegation with repeaters. Someone opened an issue on github about it. The argument is if it actually leads to better performance.
There might be a workaround but it would require jQuery. It consists of creating a special directive to be used on a parent element and register the listener on its dom node.
Here's an example, that is passed a function name to be called when a children node is clicked, and is also passed a selector to help identify which children nodes to listen to.
Since angular's jquery implementation only gives us the bind method - which is limited to registering event listeners to the actual element - we need to load jQuery to have access to either the on or delegate method.
HTML
<ul click-children='fun' selector='li'>
<li ng-repeat="s in ss" data-index="{{$index}}">{{s}}</li>
</ul>
The function defined is defined in the controller and it expects to be passed an index
$scope.fun = function(index){
console.log('hi from controller', index, $scope.ss[index]);
};
The directive uses $parse to convert an expression into a function that will be called from the event listener.
app.directive('clickChildren', function($parse){
return {
restrict: 'A',
link: function(scope, element, attrs){
var selector = attrs.selector;
var fun = $parse(attrs.clickChildren);
element.on('click', selector, function(e){
// no need to create a jQuery object to get the attribute
var idx = e.target.getAttribute('data-index');
fun(scope)(idx);
});
}
};
});
Plunker: http://plnkr.co/edit/yWCLvRSLCeshuw4j58JV?p=preview
Note: Functions can be delegated to directives using isolate scopes {fun: '&'}, which is worth a look, but this increases complexity.
Working off of jm-'s example here, I wrote a more concise and flexible version of this directive. Thought I'd share. Credit goes to jm- ;)
My version attempts to call the function name as $scope[ fn ]( e, data ), or fails gracefully.
It passes an optional json object from the element which was clicked. This allows you to use Angular expressions and pass numerous properties to the method being called.
HTML
<ul delegate-clicks="handleMenu" delegate-selector="a">
<li ng-repeat="link in links">
<a href="#" data-ng-json='{ "linkId": {{link.id}} }'>{{link.title}}</a>
</li>
</ul>
Javascript
Controller Method
$scope.handleMenu = function($event, data) {
$event.preventDefault();
$scope.activeLinkId = data.linkId;
console.log('handleMenu', data, $scope);
}
Directive Constructor
// The delegateClicks directive delegates click events to the selector provided in the delegate-selector attribute.
// It will try to call the function provided in the delegate-clicks attribute.
// Optionally, the target element can assign a data-ng-json attribute which represents a json object to pass into the function being called.
// Example json attribute: <li data-ng-json='{"key":"{{scopeValue}}" }'></li>
// Use case: Delegate click events within ng-repeater directives.
app.directive('delegateClicks', function(){
return function($scope, element, attrs) {
var fn = attrs.delegateClicks;
element.on('click', attrs.delegateSelector, function(e){
var data = angular.fromJson( angular.element( e.target ).data('ngJson') || undefined );
if( typeof $scope[ fn ] == "function" ) $scope[ fn ]( e, data );
});
};
});
I'd love to hear feedback if anyone wishes to contribute.
I didn't test the handleMenu method since I extracted this from a more complex application.
Starting from BradGreens' delegateClicks above, I've adapted some code from georg which allows me to place the handleMenu function deeper in $scope (e.g. $scope.tomato.handleMenu).
app.directive('delegateClicks', function () {
return function ($scope, element, attrs) {
var fn = attrs.delegateClicks.split('.').reduce(function ($scope, p) { return $scope[p] }, $scope);
element.on('click', attrs.delegateSelector, function (e) {
var data = angular.fromJson(angular.element(e.target).data('ngJson') || undefined);
if (typeof fn == "function") fn(e, data);
});
};
});
According to the issue on GitHub there is no performance advantage to delegating event handling.
Simply use the ng-click directive with the item, $index, and $event:
<ul>
<li ng-repeat="item in collection"
ng-click="lineClicked(item, $index, $event)">
{{item}}
</li>
</ul>
$scope.lineClicked = function(item, index, event) {
console.log("line clicked", item, index);
console.log(event.target)
});
For more information, see
AngularJS Developer Guide - $event
AngularJS ng-repeat Directive API Reference - Special properties

Resources