How to override an ng-click directive to not automatically run $apply - angularjs

Found some close questions , but not exactly the one I need to ask.
I have multiple elements with ng-click events.
For a majority of them (of a specific class), I don't need to actually run an angular digest cycle after click. The result of the click on these elements does not affect any scope variable (let's say for example they just print out a console.log).
What I want to do is to react conditionally to an ngClick, where say elements of a specific css class will not have the automatic $apply at the end.
Edit:
What I ended up doing was replace the ng-click, ng-mouseenter and ng-mouseleave with the corresponding javascript replacements.
I did this for two reasons:
1. I don't actually affect the scope variables on those clicks, so I don't need to run a digest after each (I have mouseenters, so you can imaging that generated a lot of digest cycles for no reason).
2. This is content that I load late in the page loading sequence from another source (ng-bind), so it has to be sanitized by angular and then compiled. This took a log of time (almost a second) because I have many such links, and that was holding back the display of the content.

While I highly recommend against this, as the $apply in your application shouldn't really be affecting anything (even performance). You'll have to create your own directive for this.
HTML
<div data-no-apply-click="myFunction()">
</div>
Javascript
.directive('noApplyClick', function ($parse) {
return {
compile : function ($element, attr) {
var fn = $parse(attr['noApplyClick']);
return function (scope, element, attr) {
element.on('click', function (event) {
fn(scope, {
$event : event,
$element : element
});
});
};
}
};
});
JsFiddle: http://jsfiddle.net/gj54bjsh/

Related

How does $scope.apply() work exactly in AngularJS?

I often updated model variables corresponding to DOM expression ({{}}) within the controllers. e.g.
$scope.myVar = new_value;
Some times the corresponding DOM expression {{myVar}} is updated automtically, others it's not.
I understand that sometimes we need to call $scope.$apply but...
I don't understand when I should call it
Some times I call it (let's say, just to be "sure") but I get this error (I guess since it's already being executed):
Error: [$rootScope:inprog]
http://errors.angularjs.org/1.3.6/$rootScope/inprog?p0=%24digest
Any clue?
Apply essentially "refreshes" your front end with the changes that had occurred to your scope.
Most of the time you dont need to do apply as it already is done for you.
Lets say that you do an ng-click(); Apply is done for you.
However, there are cases where apply is not triggered, and you must do it yourself.
Example with a directive:
.directive('someCheckbox', function(){
return {
restrict: 'E',
link: function($scope, $el, $attrs) {
$el.on('keypress', function(event){
event.preventDefault();
if(event.keyCode === 32 || event.keyCode === 13){
$scope.toggleCheckbox();
$scope.$apply();
}
});
}
}
})
I have made changes to my scope but no apply was done for me thus i need to do it myself.
Another example with a timeout:
$scope.getMessage = function() {
setTimeout(function() {
$scope.message = 'Fetched after two seconds';
console.log('message:' + $scope.message);
$scope.$apply(); //this triggers a $digest
}, 2000);
};
Understanding apply and digest
A good way to understand the purpose of $scope.apply() is to understand that basically it does an internal .digest() within angular to make sure the scope is in sync (double check if anything has changed, etc).
Most of the time, you never need it! Most typical angular things you'll do, ng-click for example, will automatically trigger it for you when you make any changes to the scope.
But take as an example a jQuery UI dialogBox.
Let's say you prompt the user something, and you need to update your scope when they push the OK button.
Angular isn't aware of that button, nor does it know when any event is fired on it.
Hence, this is a very common use-case for $scope.apply()
Inside of that OK buttons event, you'd simply do:
$scope.apply(function () {
// Angular is now aware that something might of changed
$scope.changeThisForMe = true;
});
In a nutshell $scope.$apply tells Angular and any watchers that values have been changed and to go back and check if there are any new values. This keeps things within the Angular context regardless of how you made a change, like in a DOM event, jQuery method, etc.

Angular: running function in controller when all directives are loaded

I'm trying to come up with some code which allows me to run a function in the controller but only once the whole dom is setup and ready (including the directives link function run etc.).
I'm currently communicating between ctrl/service and the directive via $rootScope broadcasts. The first broadcast at the time of the controller loading is not being picked up by the directive. The reason is of course that the controller loads before the directive link function runs. I've read a few similar questions on SO where people recommended on using $timeout for these calls. This unfortunately doesn't always work and I don't want to clutter my ctrl/services with lots of $timeout calls. Therefore I'm looking for another solution to my problem.
Communication pattern is as follows:
1.) Controller tells Service to prepare some data (via function call in service)
2.) Service tells directive to display data (via broadcast)
3.) Directive displays data ...or not in my case :(
EDIT:
As timing is essential in my app, I'm basically looking for a way to initiate a function in the controller as soon as all angular components have finished loading. That function in the controller will display content by assigning a value to a scope variable. At the same time it will start taking the time. I can of course only start doing that once the directives are loaded, otherwise the tmining is wrong or the directive is not yet ready to display content etc.
I've read through a blog post by Ben Nadel, which basically shows how directives are loaded. I was hoping to setup an outer directive which loads last so I can trigger the finished loading from there. Unfortunately that doesn't work as soon as any of the inner directives use a templateUrl.
http://www.bennadel.com/blog/2603-directive-controller-and-link-timing-in-angularjs.htm
Using $timeout would be terrible. Don't do that. You can't define how long a server call is going to take.
I would recommend using this pattern:
Have the controller use a service to load some data, and have the
promise in the controller assign the return data to a scope variable.
Pass that scope variable into your directive.
Setup a watch in the directive link function, when it loads it will go from undefined to desired value. Done!
// in your controller
YourService.all().then(function(data) {
$scope.data = data;
});
// in your view
<some-directive your-data="data"></some-directive>
// in your directive
angular.module('blah.directives').directive('someDirective', function() {
return {
scope: {
yourData: '='
},
link: function(scope, element, attrs) {
var watcher = scope.$watch('yourData', function() {
if(scope.yourData === undefined) return;
// at this point your data will be loaded, do work
// optionally kill watcher at this point if it's not going to update again
watcher();
});
}
}
});

is it necessary to include a destroy method in my directive

I've written a pretty simple directive that adds/removes a css class on an element when the element is clicked.
app.directive('dropdown', function() {
var open = false, element,
callback = function(){
open = !open;
if (open) {
element.addClass('open');
} else {
element.removeClass('open');
}
};
return {
scope: {},
link: function(scope, elem){
element = elem;
elem.bind('click', callback);
scope.$on('$destroy', function(){
elem.unbind('click', callback);
elem.remove();
});
}
};
});
I think that the $destroy method is probably unnecessary. Since I've used the built in jqlite the listener will be destroyed along with the element right? Also is there any benefit to calling elem.remove(). I've seen it in some examples but not sure if I see the need.
Any thoughts appreciated
C
You don't have to remove the element manually for sure. You also don't need to unbind anything from scope because it will be handled by angularjs itsef.
For jquery dom listeners:
In case you are referencing JQuery then angular will use it instead of his internal jqLite implementation. It means that the native jquery remove method will be used for the element removal. And the jquery documentation for remove says:
Similar to .empty(), the .remove() method takes elements out of the
DOM. Use .remove() when you want to remove the element itself, as well
as everything inside it. In addition to the elements themselves, all
bound events and jQuery data associated with the elements are removed.
So i think that you don't need to unbind your listeners.
But I'm not 100% sure about this:)
In your case, you should be fine since the event is bound to the element that gets removed and thus the handler gets destroyed along with the element itself. Now, if your directive binds an event to a parent outside of its own DOM element, then that would need to be removed manually on the $destroy.
However, closure can cause any object to stay alive so that's something you do need to worry about. You could introduce a new function still referencing variable objects in the functions whose scope you are trying to destroy and that prevents GC from doing what you likely want it to. Again, that won't affect your current example, but it's something to always consider.

AngularJS $animate.addClass requires $timeout

I'm having some unexpected behaviour where the $animate service's addClass method is not adding the appropriate -add and -add-active classes.
The same use of the service, wrapped in a $timeout, seems to work as expected:
http://plnkr.co/edit/3sFGp4y2WGA0HReIglR3?p=preview
Obviously, using unnecessary $timeouts is something I would like to avoid.
Am I missing something, or using the service incorrectly?
This may be way out of the ballpark of correct answers... because I've never used ngAnimate personally (I did see a presentation on it once though). However, I think it's due to the fact that you haven't waited for your image to load, which seems to be the purpose of this directive. The $timeout may give you enough time in this example to induce the animation on the correct DOM element, but may not always be correct. I believe that you should be waiting for the 'load' event on the image to use the .addClass functionality... which in this case actually turns out to be more or less the same amount of code you originally posted. The thing is that this approach will ensure that the DOM element exists, and the image has fully loaded on the page:
element.on('load', function() {
scope.$apply(function() {
$animate.addClass(element, 'preloaded', function() {
console.log('Animation finished!');
});
})
});
I'm not sure why this happens, but I've noticed that Angular needs to digest somehow before triggering an animation, usually with $timeout. Even directives that should automatically start an animation, like ngClass or ngRepeat, seem to require a timeout if they are not triggered by an event.
So this will work without a timeout:
return function (scope, element, attr) {
scope.addPreloadClick = function() {
$animate.addClass(element, 'preloaded');
};
scope.removePreloadClick = function() {
$animate.removeClass(element, 'preloaded');
};
};
Updated Plunk using ngClass and ngClick
http://plnkr.co/edit/qLFgCc0XDGJT4e7ampUW?p=preview

AngularJS and ng-repeat elements do not exist in DOM yet - getting NULL exepction

So I am trying to use an external library function to create some manipulation in the DOM in one of my controllers that my ng-repeat is connected to. The problem is the following:
I am calling that external function in my controller that adds elements to the ng-repeat array , which in term adds the new elements to the DOM. However, when I am inside the controller, the element does not exist yet, even though I have added it to the array. How can I bound an external function to be called once the element has actually been appended to the DOM, rather than when it was actually added to the array that controls the ng-repeat?
I want to fire the event once the element has actually been created. Any suggestions?
Let me know if you would like to see a fiddle of this idea.
As mentioned in the comments, a check-last directive that checks for $last can be used to determine when the last iteration of ng-repeat is executing.
.directive('checkLast', function() {
return function (scope, element, attrs) {
if (scope.$last === true) {
element.ready(function() { // or maybe $timeout
... do something ...
});
}
}
});
See also https://stackoverflow.com/a/14656888/215945

Resources